@payez/next-mvp 4.0.46 → 4.0.49
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/auth-handler.d.ts +0 -2
- package/dist/api/auth-handler.js +1 -1
- package/dist/api-handlers/admin/stats.js +24 -14
- package/dist/api-handlers/auth/refresh.d.ts +4 -6
- package/dist/api-handlers/auth/refresh.js +5 -7
- package/dist/api-handlers/auth/signout.d.ts +6 -15
- package/dist/api-handlers/auth/signout.js +9 -16
- package/dist/api-handlers/auth/update-session.d.ts +6 -15
- package/dist/api-handlers/auth/update-session.js +7 -15
- package/dist/api-handlers/auth/verify-code.d.ts +6 -15
- package/dist/api-handlers/auth/verify-code.js +7 -15
- package/dist/api-handlers/session/viability.js +2 -2
- package/dist/auth/better-auth.d.ts +3 -19
- package/dist/auth/better-auth.js +7 -13
- package/dist/client/better-auth-client.d.ts +7 -8
- package/dist/client/better-auth-client.js +3 -4
- package/dist/lib/auth-secret.d.ts +17 -0
- package/dist/lib/{nextauth-secret.js → auth-secret.js} +31 -15
- package/dist/lib/demo-mode.js +3 -1
- package/dist/lib/idp-client-config.d.ts +6 -2
- package/dist/lib/idp-client-config.js +35 -21
- package/dist/lib/secret-validation.d.ts +1 -1
- package/dist/lib/secret-validation.js +2 -2
- package/dist/lib/startup-init.d.ts +3 -3
- package/dist/lib/startup-init.js +23 -18
- package/dist/lib/test-aware-get-token.js +2 -51
- package/dist/routes/account/masked-info.d.ts +1 -1
- package/dist/routes/account/masked-info.js +1 -1
- package/dist/routes/account/send-code.d.ts +1 -1
- package/dist/routes/account/send-code.js +1 -1
- package/dist/routes/account/verify-email.d.ts +1 -1
- package/dist/routes/account/verify-email.js +1 -1
- package/dist/routes/account/verify-sms.d.ts +1 -1
- package/dist/routes/account/verify-sms.js +1 -1
- package/dist/routes/auth/refresh.js +3 -8
- package/dist/server/auth.d.ts +28 -7
- package/dist/server/auth.js +106 -55
- package/dist/server/decode-session.js +2 -2
- package/dist/vibe/hooks/index.d.ts +1 -1
- package/package.json +888 -893
- package/src/api/auth-handler.ts +0 -4
- package/src/api-handlers/admin/stats.ts +249 -238
- package/src/api-handlers/auth/refresh.ts +5 -8
- package/src/api-handlers/auth/signout.ts +9 -21
- package/src/api-handlers/auth/update-session.ts +7 -20
- package/src/api-handlers/auth/verify-code.ts +7 -20
- package/src/api-handlers/session/viability.ts +2 -2
- package/src/auth/better-auth.ts +7 -32
- package/src/client/better-auth-client.ts +3 -4
- package/src/lib/{nextauth-secret.ts → auth-secret.ts} +32 -16
- package/src/lib/demo-mode.ts +5 -1
- package/src/lib/idp-client-config.ts +42 -22
- package/src/lib/secret-validation.ts +1 -1
- package/src/lib/startup-init.ts +23 -18
- package/src/lib/test-aware-get-token.ts +2 -51
- package/src/routes/account/masked-info.ts +1 -1
- package/src/routes/account/send-code.ts +1 -1
- package/src/routes/account/verify-email.ts +1 -1
- package/src/routes/account/verify-sms.ts +1 -1
- package/src/routes/auth/refresh.ts +3 -8
- package/src/server/auth.ts +129 -22
- package/src/server/decode-session.ts +2 -2
- package/dist/lib/nextauth-secret.d.ts +0 -10
|
@@ -38,29 +38,19 @@ interface UpdateSessionResponse {
|
|
|
38
38
|
twoFactorMethod?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
interface UpdateSessionConfig {
|
|
42
|
-
nextAuthSecret?: string; // Legacy - no longer used by Better Auth
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
/**
|
|
46
|
-
* Creates an update-session handler for Next.js API routes
|
|
42
|
+
* Creates an update-session handler for Next.js API routes.
|
|
47
43
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* Better Auth resolves its session from cookies, so this handler takes no
|
|
45
|
+
* configuration. Use the default `POST` export below for typical usage.
|
|
50
46
|
*
|
|
51
47
|
* @example
|
|
52
48
|
* ```typescript
|
|
53
49
|
* // In your app's /app/api/auth/update-session/route.ts
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* export const POST = createUpdateSessionHandler({
|
|
57
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!
|
|
58
|
-
* });
|
|
50
|
+
* export { POST } from '@payez/next-mvp/api-handlers/auth/update-session';
|
|
59
51
|
* ```
|
|
60
52
|
*/
|
|
61
|
-
export function createUpdateSessionHandler(
|
|
62
|
-
const { nextAuthSecret } = config;
|
|
63
|
-
|
|
53
|
+
export function createUpdateSessionHandler() {
|
|
64
54
|
return async function POST(req: NextRequest) {
|
|
65
55
|
try {
|
|
66
56
|
let body: UpdateSessionRequest;
|
|
@@ -115,9 +105,6 @@ export function createUpdateSessionHandler(config: UpdateSessionConfig) {
|
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
/**
|
|
118
|
-
* Default export for
|
|
119
|
-
* Requires environment variable: NEXTAUTH_SECRET
|
|
108
|
+
* Default POST export — drop-in for `app/api/auth/update-session/route.ts`.
|
|
120
109
|
*/
|
|
121
|
-
export const POST = createUpdateSessionHandler(
|
|
122
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
|
|
123
|
-
});
|
|
110
|
+
export const POST = createUpdateSessionHandler();
|
|
@@ -20,29 +20,19 @@ interface VerifyCodeRequest {
|
|
|
20
20
|
refreshTokenExpires?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
interface VerifyCodeConfig {
|
|
24
|
-
nextAuthSecret?: string; // Legacy - no longer used by Better Auth
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
/**
|
|
28
|
-
* Creates a verify-code/complete-2FA handler for Next.js API routes
|
|
24
|
+
* Creates a verify-code/complete-2FA handler for Next.js API routes.
|
|
29
25
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Better Auth resolves its session from cookies, so this handler takes no
|
|
27
|
+
* configuration. Use the default `POST` export below for typical usage.
|
|
32
28
|
*
|
|
33
29
|
* @example
|
|
34
30
|
* ```typescript
|
|
35
31
|
* // In your app's /app/api/auth/verify-code/route.ts
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* export const POST = createVerifyCodeHandler({
|
|
39
|
-
* nextAuthSecret: process.env.NEXTAUTH_SECRET!
|
|
40
|
-
* });
|
|
32
|
+
* export { POST } from '@payez/next-mvp/api-handlers/auth/verify-code';
|
|
41
33
|
* ```
|
|
42
34
|
*/
|
|
43
|
-
export function createVerifyCodeHandler(
|
|
44
|
-
const { nextAuthSecret } = config;
|
|
45
|
-
|
|
35
|
+
export function createVerifyCodeHandler() {
|
|
46
36
|
return async function POST(req: NextRequest) {
|
|
47
37
|
try {
|
|
48
38
|
let body: VerifyCodeRequest;
|
|
@@ -117,9 +107,6 @@ export function createVerifyCodeHandler(config: VerifyCodeConfig) {
|
|
|
117
107
|
}
|
|
118
108
|
|
|
119
109
|
/**
|
|
120
|
-
* Default export for
|
|
121
|
-
* Requires environment variable: NEXTAUTH_SECRET
|
|
110
|
+
* Default POST export — drop-in for `app/api/auth/verify-code/route.ts`.
|
|
122
111
|
*/
|
|
123
|
-
export const POST = createVerifyCodeHandler(
|
|
124
|
-
nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
|
|
125
|
-
});
|
|
112
|
+
export const POST = createVerifyCodeHandler();
|
|
@@ -12,8 +12,8 @@ import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
|
12
12
|
|
|
13
13
|
export async function GET(req: NextRequest) {
|
|
14
14
|
try {
|
|
15
|
-
// Ensure initialization is complete
|
|
16
|
-
if (!process.env.NEXTAUTH_SECRET) {
|
|
15
|
+
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
16
|
+
if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
|
|
17
17
|
try {
|
|
18
18
|
await ensureInitialized();
|
|
19
19
|
} catch (error) {
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -48,49 +48,25 @@ export function buildBetterAuthProviders(
|
|
|
48
48
|
return providers;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* Optional extra plugins for createBetterAuthInstance.
|
|
53
|
-
* Use to add credentials/custom signin endpoints from the host app.
|
|
54
|
-
*/
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
-
export type BetterAuthExtraPlugins = any[];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Optional overrides for createBetterAuthInstance.
|
|
60
|
-
*/
|
|
61
|
-
export interface BetterAuthInstanceOptions {
|
|
62
|
-
/** Path suffix for the BA mount (default: '/api/auth'). Use '/api/ba-auth' for migration scenarios. */
|
|
63
|
-
basePath?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
51
|
/**
|
|
67
52
|
* Create Better Auth instance from IDP config.
|
|
68
53
|
*
|
|
69
54
|
* No database — runs in stateless mode with JWE cookie cache.
|
|
70
55
|
* Call after getIDPClientConfig() resolves.
|
|
71
|
-
*
|
|
72
|
-
* @param idpConfig IDP client config (from getIDPClientConfig)
|
|
73
|
-
* @param extraPlugins Optional plugins to add (e.g., credentials plugin from host app)
|
|
74
|
-
* @param options Optional overrides (e.g., basePath for migration scenarios)
|
|
75
56
|
*/
|
|
76
|
-
export function createBetterAuthInstance(
|
|
77
|
-
idpConfig: IDPClientConfig,
|
|
78
|
-
extraPlugins: BetterAuthExtraPlugins = [],
|
|
79
|
-
options: BetterAuthInstanceOptions = {}
|
|
80
|
-
) {
|
|
57
|
+
export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
|
|
81
58
|
const appSlug = idpConfig.clientSlug || getAppSlug();
|
|
82
|
-
const basePath = options.basePath || '/api/auth';
|
|
83
59
|
|
|
84
60
|
// Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
|
|
85
|
-
//
|
|
61
|
+
// Must include /api/auth since that's where the catch-all route is mounted
|
|
86
62
|
const rawBaseURL = process.env.BETTER_AUTH_URL
|
|
87
63
|
|| idpConfig.baseClientUrl
|
|
88
64
|
|| `http://localhost:${process.env.PORT || '3000'}`;
|
|
89
|
-
const baseURL = rawBaseURL.replace(/\/+$/, '') +
|
|
65
|
+
const baseURL = rawBaseURL.replace(/\/+$/, '') + '/api/auth';
|
|
90
66
|
|
|
91
67
|
return betterAuth({
|
|
92
68
|
baseURL,
|
|
93
|
-
secret: idpConfig.
|
|
69
|
+
secret: idpConfig.authSecret as string,
|
|
94
70
|
|
|
95
71
|
socialProviders: buildBetterAuthProviders(idpConfig),
|
|
96
72
|
|
|
@@ -147,7 +123,6 @@ export function createBetterAuthInstance(
|
|
|
147
123
|
},
|
|
148
124
|
|
|
149
125
|
plugins: [
|
|
150
|
-
...extraPlugins,
|
|
151
126
|
nextCookies(),
|
|
152
127
|
],
|
|
153
128
|
});
|
|
@@ -173,12 +148,12 @@ let initPromise: Promise<any> | null = null;
|
|
|
173
148
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
149
|
export { cachedInstance as __betterAuthInstance };
|
|
175
150
|
|
|
176
|
-
export async function getBetterAuthInstance(
|
|
151
|
+
export async function getBetterAuthInstance() {
|
|
177
152
|
if (cachedInstance) return cachedInstance;
|
|
178
153
|
|
|
179
154
|
if (!initPromise) {
|
|
180
155
|
initPromise = getIDPClientConfig(true).then(config => {
|
|
181
|
-
const instance = createBetterAuthInstance(config
|
|
156
|
+
const instance = createBetterAuthInstance(config);
|
|
182
157
|
cachedInstance = instance;
|
|
183
158
|
console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
|
|
184
159
|
return instance;
|
|
@@ -287,7 +262,7 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
287
262
|
}
|
|
288
263
|
|
|
289
264
|
// Build IDP token data
|
|
290
|
-
const requiresTwoFactor = result.
|
|
265
|
+
const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
|
|
291
266
|
const idpTokenData = {
|
|
292
267
|
idpAccessToken: result.access_token,
|
|
293
268
|
idpRefreshToken: result.refresh_token,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Better Auth Client
|
|
2
|
+
* Better Auth Client.
|
|
3
3
|
*
|
|
4
|
-
* Drop-in replacement for next-auth/react hooks and functions.
|
|
5
4
|
* Import from '@payez/next-mvp/client/better-auth-client'.
|
|
6
5
|
*
|
|
7
|
-
* Includes useSessionCompat() — returns
|
|
8
|
-
*
|
|
6
|
+
* Includes useSessionCompat() — returns a { data, status } shape so existing
|
|
7
|
+
* components written against the legacy hook don't need destructure changes.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { createAuthClient } from 'better-auth/react';
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
import {
|
|
2
|
+
import { validateAuthSecret } from './secret-validation';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
|
|
5
5
|
let cachedSecret: string | null = null;
|
|
6
6
|
let lastFetchedAt = 0;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Resolve the
|
|
9
|
+
* Resolve the Better Auth signing secret (server-only).
|
|
10
10
|
*
|
|
11
11
|
* Priority:
|
|
12
|
-
* 1) Use process.env.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 1) Use process.env.BETTER_AUTH_SECRET (preferred) or NEXTAUTH_SECRET (legacy)
|
|
13
|
+
* if present — allows overrides/production via env.
|
|
14
|
+
* 2) Fetch from IDP broker endpoint — IDP handles all Key Vault/signing.
|
|
15
|
+
* 3) Cache result in-memory and set process.env.BETTER_AUTH_SECRET for
|
|
16
|
+
* subsequent calls.
|
|
17
|
+
*
|
|
18
|
+
* NOTE on naming: this secret is the cryptographic key Better Auth uses to
|
|
19
|
+
* sign session JWTs. The IDP backend still names the broker endpoint and
|
|
20
|
+
* response field with the legacy "next-auth" / "nextAuthSecret" names; we
|
|
21
|
+
* read both new and legacy on the wire during the migration window.
|
|
15
22
|
*/
|
|
16
|
-
export async function
|
|
17
|
-
// Check if already in environment
|
|
18
|
-
|
|
23
|
+
export async function resolveAuthSecret(): Promise<string> {
|
|
24
|
+
// Check if already in environment (prefer new name, fall back to legacy)
|
|
25
|
+
const envSecret =
|
|
26
|
+
(process.env.BETTER_AUTH_SECRET && process.env.BETTER_AUTH_SECRET.trim() !== ''
|
|
27
|
+
? process.env.BETTER_AUTH_SECRET
|
|
28
|
+
: undefined) ||
|
|
29
|
+
(process.env.NEXTAUTH_SECRET && process.env.NEXTAUTH_SECRET.trim() !== ''
|
|
30
|
+
? process.env.NEXTAUTH_SECRET
|
|
31
|
+
: undefined);
|
|
32
|
+
if (envSecret) {
|
|
19
33
|
// Silent - already configured
|
|
20
|
-
return
|
|
34
|
+
return envSecret;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
// Check if cached and fresh (within 5 minutes)
|
|
@@ -74,7 +88,8 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
74
88
|
throw new Error('IDP did not return a valid signed client assertion');
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
// Step 2: Use the signed assertion to fetch the
|
|
91
|
+
// Step 2: Use the signed assertion to fetch the auth secret
|
|
92
|
+
// (Endpoint is still served at /next-auth/secret on the IDP — legacy path.)
|
|
78
93
|
|
|
79
94
|
const proxyUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/next-auth/secret`);
|
|
80
95
|
|
|
@@ -98,24 +113,25 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
98
113
|
const proxyBody: any = await proxyResp.json().catch(() => ({}));
|
|
99
114
|
|
|
100
115
|
const secret = (proxyBody?.data?.secret ?? proxyBody?.secret) as string | undefined;
|
|
101
|
-
const configuration = (proxyBody?.data?.configuration ?? proxyBody?.configuration) as any | undefined;
|
|
102
|
-
|
|
103
116
|
// Configuration is available but we don't log it verbosely
|
|
104
117
|
|
|
105
118
|
if (!secret || typeof secret !== 'string') {
|
|
106
|
-
throw new Error('Proxy did not return a valid
|
|
119
|
+
throw new Error('Proxy did not return a valid auth secret');
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
const validation =
|
|
122
|
+
const validation = validateAuthSecret(secret);
|
|
110
123
|
if (!validation.valid) {
|
|
111
|
-
throw new Error(`Fetched
|
|
124
|
+
throw new Error(`Fetched auth secret failed validation: ${validation.reason}`);
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
cachedSecret = secret;
|
|
115
128
|
lastFetchedAt = Date.now();
|
|
129
|
+
process.env.BETTER_AUTH_SECRET = secret;
|
|
130
|
+
// Also set legacy name during transition so any consumer still reading
|
|
131
|
+
// process.env.NEXTAUTH_SECRET keeps working until they upgrade.
|
|
116
132
|
process.env.NEXTAUTH_SECRET = secret;
|
|
117
133
|
|
|
118
|
-
console.log('[
|
|
134
|
+
console.log('[AUTH-SECRET] Resolved from IDP (length:', secret.length + ')');
|
|
119
135
|
|
|
120
136
|
return secret;
|
|
121
137
|
}
|
package/src/lib/demo-mode.ts
CHANGED
|
@@ -9,5 +9,9 @@ export function isDemoMode(): boolean {
|
|
|
9
9
|
|
|
10
10
|
export function isAuthConfigured(): boolean {
|
|
11
11
|
if (isDemoMode()) return false;
|
|
12
|
-
return !!(
|
|
12
|
+
return !!(
|
|
13
|
+
process.env.BETTER_AUTH_SECRET ||
|
|
14
|
+
process.env.NEXTAUTH_SECRET ||
|
|
15
|
+
(process.env.NEXT_CLIENT_ID && process.env.NEXT_CLIENT_PRIVATE_KEY_PEM)
|
|
16
|
+
);
|
|
13
17
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OAuth provider credentials (from Key Vault)
|
|
6
6
|
* - 2FA/MFA settings
|
|
7
7
|
* - Session configuration
|
|
8
|
-
* -
|
|
8
|
+
* - Better Auth signing secret
|
|
9
9
|
* - Branding
|
|
10
10
|
*
|
|
11
11
|
* CACHING STRATEGY:
|
|
@@ -58,7 +58,11 @@ export interface BrandingConfig {
|
|
|
58
58
|
export interface IDPClientConfig {
|
|
59
59
|
clientId: string;
|
|
60
60
|
clientSlug: string;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Cryptographic secret used by Better Auth to sign session JWTs.
|
|
63
|
+
* Historically named "nextAuthSecret" — kept under the new name now.
|
|
64
|
+
*/
|
|
65
|
+
authSecret: string;
|
|
62
66
|
configCacheTtlSeconds: number;
|
|
63
67
|
oauthProviders: OAuthProviderConfig[];
|
|
64
68
|
authSettings: AuthSettings;
|
|
@@ -170,14 +174,15 @@ export async function getIDPClientConfig(forceRefresh: boolean = false): Promise
|
|
|
170
174
|
cachedConfig = redisConfig;
|
|
171
175
|
cacheExpiry = Date.now() + ((redisConfig.configCacheTtlSeconds || 300) * 1000);
|
|
172
176
|
|
|
173
|
-
// Set
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
// Set BETTER_AUTH_SECRET from cached config (also set legacy
|
|
178
|
+
// NEXTAUTH_SECRET during the rename transition).
|
|
179
|
+
if (redisConfig.authSecret) {
|
|
180
|
+
process.env.BETTER_AUTH_SECRET = redisConfig.authSecret;
|
|
181
|
+
process.env.NEXTAUTH_SECRET = redisConfig.authSecret;
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from cached config
|
|
179
|
-
//
|
|
180
|
-
// Only set if not already defined (allows deployment override for beta/staging)
|
|
184
|
+
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from cached config.
|
|
185
|
+
// Only set if not already defined (allows deployment override for beta/staging).
|
|
181
186
|
if (redisConfig.baseClientUrl && !process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL) {
|
|
182
187
|
process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL = redisConfig.baseClientUrl;
|
|
183
188
|
}
|
|
@@ -211,16 +216,17 @@ export async function getIDPClientConfig(forceRefresh: boolean = false): Promise
|
|
|
211
216
|
// Store in Redis for persistence across module reloads
|
|
212
217
|
await setConfigInRedis(config);
|
|
213
218
|
|
|
214
|
-
// Set
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
// Set BETTER_AUTH_SECRET from config (also set legacy
|
|
220
|
+
// NEXTAUTH_SECRET during the rename transition).
|
|
221
|
+
if (config.authSecret) {
|
|
222
|
+
process.env.BETTER_AUTH_SECRET = config.authSecret;
|
|
223
|
+
process.env.NEXTAUTH_SECRET = config.authSecret;
|
|
217
224
|
} else {
|
|
218
|
-
throw new Error('[IDP_CONFIG] FATAL: IDP did not return
|
|
225
|
+
throw new Error('[IDP_CONFIG] FATAL: IDP did not return authSecret');
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from config
|
|
222
|
-
//
|
|
223
|
-
// Only set if not already defined (allows deployment override for beta/staging)
|
|
228
|
+
// Set IDENTITY_CLIENT_BASE_EXTERNAL_URL from config.
|
|
229
|
+
// Only set if not already defined (allows deployment override for beta/staging).
|
|
224
230
|
if (config.baseClientUrl && !process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL) {
|
|
225
231
|
process.env.IDENTITY_CLIENT_BASE_EXTERNAL_URL = config.baseClientUrl;
|
|
226
232
|
console.log("[IDP_CONFIG] Set IDENTITY_CLIENT_BASE_EXTERNAL_URL:", config.baseClientUrl);
|
|
@@ -305,7 +311,14 @@ async function fetchConfigFromInternalIDP(internalIdpUrl: string, clientIdStr: s
|
|
|
305
311
|
const config: IDPClientConfig = {
|
|
306
312
|
clientId: String(rawClientId),
|
|
307
313
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
308
|
-
|
|
314
|
+
// Wire compatibility: accept new authSecret first, fall back to legacy
|
|
315
|
+
// nextAuthSecret/next_auth_secret while IDP rename rolls out.
|
|
316
|
+
authSecret:
|
|
317
|
+
configData.authSecret ??
|
|
318
|
+
configData.auth_secret ??
|
|
319
|
+
configData.nextAuthSecret ??
|
|
320
|
+
configData.next_auth_secret ??
|
|
321
|
+
'',
|
|
309
322
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
310
323
|
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p: any) => ({
|
|
311
324
|
provider: p.provider ?? '',
|
|
@@ -336,8 +349,8 @@ async function fetchConfigFromInternalIDP(internalIdpUrl: string, clientIdStr: s
|
|
|
336
349
|
baseClientUrl: configData.baseClientUrl ?? configData.base_client_url ?? configData.BaseClientUrl
|
|
337
350
|
};
|
|
338
351
|
|
|
339
|
-
if (!config.
|
|
340
|
-
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return
|
|
352
|
+
if (!config.authSecret) {
|
|
353
|
+
throw new Error('[IDP_CONFIG] FATAL: Internal IDP did not return authSecret');
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
console.log(`[IDP_CONFIG] Internal IDP config loaded for ${clientIdStr}`);
|
|
@@ -464,11 +477,18 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
464
477
|
throw new Error(`[IDP_CONFIG] FATAL: IDP response missing clientId/client_id. Got: ${JSON.stringify(Object.keys(configData))}`);
|
|
465
478
|
}
|
|
466
479
|
|
|
467
|
-
// Map response to our interface (IDP
|
|
480
|
+
// Map response to our interface (IDP returns camelCase or snake_case).
|
|
481
|
+
// Wire compatibility: accept new authSecret first, fall back to legacy
|
|
482
|
+
// nextAuthSecret/next_auth_secret while IDP rename rolls out.
|
|
468
483
|
const config: IDPClientConfig = {
|
|
469
484
|
clientId: String(rawClientId),
|
|
470
485
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
471
|
-
|
|
486
|
+
authSecret:
|
|
487
|
+
configData.authSecret ??
|
|
488
|
+
configData.auth_secret ??
|
|
489
|
+
configData.nextAuthSecret ??
|
|
490
|
+
configData.next_auth_secret ??
|
|
491
|
+
'',
|
|
472
492
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
473
493
|
oauthProviders: (configData.oauthProviders ?? configData.oauth_providers ?? []).map((p: any) => ({
|
|
474
494
|
provider: p.provider ?? '',
|
|
@@ -517,8 +537,8 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
517
537
|
if (!config.clientId) {
|
|
518
538
|
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
519
539
|
}
|
|
520
|
-
if (!config.
|
|
521
|
-
throw new Error('[IDP_CONFIG] FATAL:
|
|
540
|
+
if (!config.authSecret) {
|
|
541
|
+
throw new Error('[IDP_CONFIG] FATAL: authSecret is empty after parsing');
|
|
522
542
|
}
|
|
523
543
|
|
|
524
544
|
// Success - reset failure tracking
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function validateAuthSecret(secret: string): { valid: boolean; reason?: string } {
|
|
2
2
|
if (!secret || typeof secret !== 'string') return { valid: false, reason: 'missing' };
|
|
3
3
|
if (secret.length < 32) return { valid: false, reason: 'too_short' };
|
|
4
4
|
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^a-zA-Z0-9]/];
|
package/src/lib/startup-init.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* This module ensures that critical initialization tasks are completed
|
|
5
5
|
* before the application serves requests.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
7
|
+
* Uses unified IDP client config for:
|
|
8
|
+
* - BETTER_AUTH_SECRET (the Better Auth signing secret)
|
|
9
9
|
* - OAuth provider configuration
|
|
10
10
|
* - Auth settings (2FA, session timeouts, etc.)
|
|
11
11
|
*/
|
|
@@ -75,7 +75,7 @@ export function logStartupStatus(): void {
|
|
|
75
75
|
console.log('║ 🚀 PayEz Next MVP - Starting Up ║');
|
|
76
76
|
console.log('║ ║');
|
|
77
77
|
console.log('║ Async initialization in progress... ║');
|
|
78
|
-
console.log('║ - Resolving
|
|
78
|
+
console.log('║ - Resolving BETTER_AUTH_SECRET from IDP ║');
|
|
79
79
|
console.log('║ - Verifying environment configuration ║');
|
|
80
80
|
console.log('║ ║');
|
|
81
81
|
console.log('║ Check logs below for detailed initialization status: ║');
|
|
@@ -117,16 +117,21 @@ async function performInitialization(): Promise<void> {
|
|
|
117
117
|
console.log('[STARTUP] Client config loaded successfully');
|
|
118
118
|
console.log('[STARTUP] - Client ID:', config.clientId);
|
|
119
119
|
console.log('[STARTUP] - Client Slug:', config.clientSlug);
|
|
120
|
-
console.log('[STARTUP] - Secret length:', config.
|
|
120
|
+
console.log('[STARTUP] - Secret length:', config.authSecret?.length || 0, 'chars');
|
|
121
121
|
console.log('[STARTUP] - OAuth Providers:', config.oauthProviders?.filter(p => p.enabled).map(p => p.provider).join(', ') || 'none');
|
|
122
122
|
console.log('[STARTUP] - Require 2FA:', config.authSettings?.require2FA);
|
|
123
123
|
console.log('[STARTUP] - Cache TTL:', config.configCacheTtlSeconds, 'seconds');
|
|
124
124
|
console.log('[STARTUP] - Base Client URL:', config.baseClientUrl || '(not set)');
|
|
125
125
|
|
|
126
|
-
// Set
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// Set BETTER_AUTH_SECRET from IDP response if not already set.
|
|
127
|
+
// Also mirror to legacy NEXTAUTH_SECRET during the rename transition
|
|
128
|
+
// so any consumer code still reading the old name keeps working.
|
|
129
|
+
if (config.authSecret && !process.env.BETTER_AUTH_SECRET) {
|
|
130
|
+
process.env.BETTER_AUTH_SECRET = config.authSecret;
|
|
131
|
+
console.log('[STARTUP] Set BETTER_AUTH_SECRET from IDP config');
|
|
132
|
+
}
|
|
133
|
+
if (config.authSecret && !process.env.NEXTAUTH_SECRET) {
|
|
134
|
+
process.env.NEXTAUTH_SECRET = config.authSecret;
|
|
130
135
|
}
|
|
131
136
|
} catch (error) {
|
|
132
137
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -136,16 +141,16 @@ async function performInitialization(): Promise<void> {
|
|
|
136
141
|
console.error('[STARTUP] No fallback available for auth secret resolution');
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
// Step 2: Verify
|
|
140
|
-
console.log('[STARTUP] Step 2/2: Verifying
|
|
144
|
+
// Step 2: Verify BETTER_AUTH_SECRET is available - FAIL FAST if not
|
|
145
|
+
console.log('[STARTUP] Step 2/2: Verifying BETTER_AUTH_SECRET...');
|
|
141
146
|
|
|
142
|
-
const secret = process.env.NEXTAUTH_SECRET;
|
|
147
|
+
const secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
|
|
143
148
|
if (!secret || secret.trim() === '') {
|
|
144
149
|
console.error('');
|
|
145
150
|
console.error('╔══════════════════════════════════════════════════════════════╗');
|
|
146
|
-
console.error('║ ❌ FATAL:
|
|
151
|
+
console.error('║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║');
|
|
147
152
|
console.error('║ ║');
|
|
148
|
-
console.error('║ The app cannot start without a valid
|
|
153
|
+
console.error('║ The app cannot start without a valid auth signing secret. ║');
|
|
149
154
|
console.error('║ This should be fetched from IDP at startup. ║');
|
|
150
155
|
console.error('║ ║');
|
|
151
156
|
console.error('║ Possible causes: ║');
|
|
@@ -155,10 +160,10 @@ async function performInitialization(): Promise<void> {
|
|
|
155
160
|
console.error('║ • Network connectivity issue ║');
|
|
156
161
|
console.error('╚══════════════════════════════════════════════════════════════╝');
|
|
157
162
|
console.error('');
|
|
158
|
-
throw new Error('FATAL:
|
|
163
|
+
throw new Error('FATAL: BETTER_AUTH_SECRET not available - cannot start without valid secret from IDP');
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
console.log('[STARTUP]
|
|
166
|
+
console.log('[STARTUP] BETTER_AUTH_SECRET verified (' + secret.length + ' chars)');
|
|
162
167
|
|
|
163
168
|
// Step 3: Validate cookie name consistency
|
|
164
169
|
// This catches bugs where getJwtCookieName() returns a different name than
|
|
@@ -192,9 +197,9 @@ async function performInitialization(): Promise<void> {
|
|
|
192
197
|
|
|
193
198
|
console.error(`
|
|
194
199
|
╔══════════════════════════════════════════════════════════════╗
|
|
195
|
-
║ ❌ FATAL:
|
|
200
|
+
║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
|
|
196
201
|
║ ║
|
|
197
|
-
║ The app cannot start without a valid
|
|
202
|
+
║ The app cannot start without a valid auth signing secret. ║
|
|
198
203
|
║ This should be fetched from IDP at startup. ║
|
|
199
204
|
║ ║
|
|
200
205
|
${connectionLine}║ Possible causes: ║
|
|
@@ -225,7 +230,7 @@ export function getStartupIDPConfig(): IDPClientConfig | null {
|
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
/**
|
|
228
|
-
* Check if initialization failed (
|
|
233
|
+
* Check if initialization failed (auth signing secret couldn't be retrieved)
|
|
229
234
|
*/
|
|
230
235
|
export function isInitializationFailed(): boolean {
|
|
231
236
|
return initializationFailed;
|
|
@@ -6,11 +6,11 @@ import { getSessionCookieName } from './app-slug';
|
|
|
6
6
|
export async function getTokenTestAware(req: NextRequest): Promise<any> {
|
|
7
7
|
if (process.env.TEST_MODE === 'true') {
|
|
8
8
|
try {
|
|
9
|
-
let secret = process.env.NEXTAUTH_SECRET;
|
|
9
|
+
let secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
|
|
10
10
|
if (!secret || secret.trim() === '') {
|
|
11
11
|
const { getIDPClientConfig } = await import('./idp-client-config');
|
|
12
12
|
const idpConfig = await getIDPClientConfig();
|
|
13
|
-
secret = idpConfig.
|
|
13
|
+
secret = idpConfig.authSecret;
|
|
14
14
|
}
|
|
15
15
|
// Use app-slug prefixed cookie name
|
|
16
16
|
const cookieName = getSessionCookieName();
|
|
@@ -37,54 +37,5 @@ export async function getTokenTestAware(req: NextRequest): Promise<any> {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// Fallback for legacy NextAuth-based sites: try next-auth/jwt with the
|
|
41
|
-
// app-slug-prefixed cookie name. Uses runtime require so consumers without
|
|
42
|
-
// next-auth installed are unaffected.
|
|
43
|
-
try {
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-eval
|
|
45
|
-
const dynamicRequire = eval('require') as NodeRequire;
|
|
46
|
-
let nextAuthJwt: any = null;
|
|
47
|
-
try {
|
|
48
|
-
nextAuthJwt = dynamicRequire('next-auth/jwt');
|
|
49
|
-
} catch {
|
|
50
|
-
return null; // next-auth not installed → BA-only consumer
|
|
51
|
-
}
|
|
52
|
-
if (!nextAuthJwt?.getToken) return null;
|
|
53
|
-
|
|
54
|
-
const { resolveNextAuthSecret } = await import('./nextauth-secret');
|
|
55
|
-
const secret = await resolveNextAuthSecret();
|
|
56
|
-
if (!secret) return null;
|
|
57
|
-
|
|
58
|
-
const cookieName = getSessionCookieName();
|
|
59
|
-
let nextAuthToken = await nextAuthJwt.getToken({
|
|
60
|
-
req,
|
|
61
|
-
secret,
|
|
62
|
-
cookieName,
|
|
63
|
-
secureCookie: false,
|
|
64
|
-
});
|
|
65
|
-
if (nextAuthToken) {
|
|
66
|
-
logger.debug('[GET_TOKEN] Resolved via NextAuth JWT (cookieName=' + cookieName + ')');
|
|
67
|
-
return nextAuthToken;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Try secure cookie variant for production
|
|
71
|
-
const { getSecureSessionCookieName } = await import('./app-slug');
|
|
72
|
-
const secureCookieName = getSecureSessionCookieName();
|
|
73
|
-
nextAuthToken = await nextAuthJwt.getToken({
|
|
74
|
-
req,
|
|
75
|
-
secret,
|
|
76
|
-
cookieName: secureCookieName,
|
|
77
|
-
secureCookie: true,
|
|
78
|
-
});
|
|
79
|
-
if (nextAuthToken) {
|
|
80
|
-
logger.debug('[GET_TOKEN] Resolved via NextAuth JWT (secure cookie)');
|
|
81
|
-
return nextAuthToken;
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
logger.debug('[GET_TOKEN] NextAuth fallback error', {
|
|
85
|
-
error: error instanceof Error ? error.message : String(error),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
40
|
return null;
|
|
90
41
|
}
|
|
@@ -28,7 +28,7 @@ export { POST } from '../../api-handlers/account/masked-info';
|
|
|
28
28
|
* Environment variables used:
|
|
29
29
|
* - IDP_URL or NEXT_PUBLIC_IDP_URL (default: http://localhost:32785)
|
|
30
30
|
* - CLIENT_ID or NEXT_PUBLIC_IDP_CLIENT_ID (required)
|
|
31
|
-
* -
|
|
31
|
+
* - BETTER_AUTH_SECRET (required — fetched from IDP at startup)
|
|
32
32
|
*
|
|
33
33
|
* Returns:
|
|
34
34
|
* - Masked email addresses
|
|
@@ -31,7 +31,7 @@ export { POST } from '../../api-handlers/account/send-code';
|
|
|
31
31
|
* Environment variables used:
|
|
32
32
|
* - IDP_URL or NEXT_PUBLIC_IDP_URL (default: http://localhost:32785)
|
|
33
33
|
* - CLIENT_ID or NEXT_PUBLIC_IDP_CLIENT_ID (required)
|
|
34
|
-
* -
|
|
34
|
+
* - BETTER_AUTH_SECRET (required — fetched from IDP at startup)
|
|
35
35
|
*
|
|
36
36
|
* Returns:
|
|
37
37
|
* - Success status
|
|
@@ -30,7 +30,7 @@ export { POST } from '../../api-handlers/account/verify-email';
|
|
|
30
30
|
* Environment variables used:
|
|
31
31
|
* - IDP_URL or NEXT_PUBLIC_IDP_URL (default: http://localhost:32785)
|
|
32
32
|
* - CLIENT_ID or NEXT_PUBLIC_IDP_CLIENT_ID (required)
|
|
33
|
-
* -
|
|
33
|
+
* - BETTER_AUTH_SECRET (required — fetched from IDP at startup)
|
|
34
34
|
*
|
|
35
35
|
* Returns:
|
|
36
36
|
* - Upgraded access token with MFA claim
|