@payez/next-mvp 4.1.10 → 4.1.12

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.
Files changed (89) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.js +9 -18
  3. package/dist/client/better-auth-client.d.ts +14 -2
  4. package/dist/lib/ensure-fresh-access-token.js +7 -30
  5. package/dist/lib/session-store.d.ts +0 -1
  6. package/dist/lib/session-store.js +29 -33
  7. package/dist/lib/startup-init.js +19 -19
  8. package/package.json +4 -3
  9. package/scripts/check-internal-url-usage.sh +73 -73
  10. package/src/api/index.ts +17 -17
  11. package/src/api-handlers/account/masked-info.ts +45 -45
  12. package/src/api-handlers/account/profile.ts +80 -80
  13. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  14. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  15. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  16. package/src/api-handlers/account/reset-password.ts +23 -23
  17. package/src/api-handlers/account/send-code.ts +76 -76
  18. package/src/api-handlers/account/validate-password.ts +118 -118
  19. package/src/api-handlers/admin/stats.ts +249 -249
  20. package/src/api-handlers/anon/preferences.ts +123 -123
  21. package/src/api-handlers/auth/jwks.ts +20 -20
  22. package/src/api-handlers/auth/login.ts +240 -240
  23. package/src/api-handlers/auth/refresh.ts +15 -38
  24. package/src/api-handlers/auth/validate.ts +43 -43
  25. package/src/api-handlers/session/viability.ts +142 -142
  26. package/src/auth/better-auth.ts +9 -14
  27. package/src/auth/unauthenticated-routes.ts +2 -2
  28. package/src/client/better-auth-client.ts +70 -70
  29. package/src/client/fetchWithSession.ts +21 -21
  30. package/src/client/index.ts +13 -13
  31. package/src/client/useAnonSession.ts +131 -131
  32. package/src/components/admin/VibeAdminContext.tsx +87 -87
  33. package/src/config/logging-config.ts +82 -82
  34. package/src/config/unauthenticated-routes.ts +19 -19
  35. package/src/edge/internal-api-url.ts +65 -65
  36. package/src/edge/middleware.ts +42 -42
  37. package/src/hooks/useProfile.ts +75 -75
  38. package/src/lib/anon-session.ts +213 -213
  39. package/src/lib/auth-secret.ts +137 -137
  40. package/src/lib/demo-mode.ts +17 -17
  41. package/src/lib/ensure-fresh-access-token.ts +7 -30
  42. package/src/lib/idp-client-config.ts +559 -559
  43. package/src/lib/idp-fetch.ts +126 -126
  44. package/src/lib/internal-api.ts +171 -171
  45. package/src/lib/jwt-decode-client.ts +45 -45
  46. package/src/lib/rate-limit-service.ts +9 -9
  47. package/src/lib/redis.ts +26 -26
  48. package/src/lib/refresh-token-validator.ts +64 -64
  49. package/src/lib/secret-validation.ts +8 -8
  50. package/src/lib/session-store.ts +692 -695
  51. package/src/lib/session.ts +34 -34
  52. package/src/lib/startup-init.ts +251 -251
  53. package/src/lib/token-expiry.ts +40 -40
  54. package/src/lib/token-lifecycle.ts +470 -470
  55. package/src/lib/types/api-responses.ts +336 -336
  56. package/src/middleware/auth-decision.ts +43 -43
  57. package/src/models/DecodedAccessToken.ts +16 -16
  58. package/src/models/SessionModel.ts +264 -264
  59. package/src/pages/admin-roles/index.ts +9 -9
  60. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  61. package/src/routes/account/index.ts +31 -31
  62. package/src/routes/account/masked-info.ts +36 -36
  63. package/src/routes/account/send-code.ts +39 -39
  64. package/src/routes/account/verify-email.ts +40 -40
  65. package/src/routes/account/verify-sms.ts +40 -40
  66. package/src/routes/auth/index.ts +22 -22
  67. package/src/routes/auth/refresh.ts +48 -48
  68. package/src/routes/auth/session.ts +166 -166
  69. package/src/routes/index.ts +17 -17
  70. package/src/routes/session/index.ts +7 -7
  71. package/src/routes/session/refresh-viability.ts +17 -17
  72. package/src/server/auth.ts +373 -373
  73. package/src/server/decode-session.ts +202 -202
  74. package/src/services/signalrActivityService.ts +258 -258
  75. package/src/theme/ThemeProvider.tsx +39 -39
  76. package/src/theme/default.ts +33 -33
  77. package/src/theme/index.ts +31 -31
  78. package/src/theme/types.ts +69 -69
  79. package/src/theme/useTheme.ts +57 -57
  80. package/src/theme/utils.ts +40 -40
  81. package/src/types/api.ts +12 -12
  82. package/src/types/auth.d.ts +23 -23
  83. package/src/types/auth.ts +22 -22
  84. package/src/types/logging.ts +11 -11
  85. package/src/types/recovery.ts +54 -54
  86. package/src/utils/api.ts +353 -353
  87. package/src/utils/circuitBreaker.ts +40 -40
  88. package/src/utils/error-message.ts +108 -108
  89. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -371,7 +371,7 @@ function createRefreshHandler(config) {
371
371
  const errorMessage = idpError.message || 'Token refresh failed';
372
372
  // CRITICAL: 401 means token is definitively invalid - always discard
373
373
  // This prevents redirect loops where viability check keeps returning canRefresh:true
374
- let discardToken = refreshResponse.status === 401 || idpError.discard_token === true;
374
+ const discardToken = refreshResponse.status === 401 || idpError.discard_token === true;
375
375
  const retryable = refreshResponse.status !== 401 && idpError.retryable === true;
376
376
  const resolution = idpError.resolution || null;
377
377
  // ============================================================================
@@ -408,43 +408,20 @@ function createRefreshHandler(config) {
408
408
  console.error('[AUTH_REFRESH] Full IDP error response:', JSON.stringify(responseData, null, 2).substring(0, 1000));
409
409
  // If IDP signals to discard token, clear it from Redis
410
410
  if (discardToken) {
411
- // Race-safe discard (CAS): only clear if the refresh token we sent is STILL
412
- // the current one. A concurrent refresh may have already rotated it — our 401
413
- // is then stale, and wiping here would silently log out a healthy session
414
- // (the rotation-race wipe, QA 6873 site 2). Skip the wipe + drop terminal if changed.
415
- const postRefresh = await (0, session_store_1.getSession)(sessionToken);
416
- const tokenUnchanged = !!originalRefreshToken && postRefresh?.idpRefreshToken === originalRefreshToken;
417
- if (tokenUnchanged) {
418
- console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: IDP signaled discard_token', {
419
- reason: 'IDP_DISCARD_TOKEN',
420
- errorCode,
421
- userId,
422
- sessionToken: sessionToken.substring(0, 8) + '...',
423
- hadRefreshToken: !!currentSession.idpRefreshToken,
424
- stack: new Error().stack
425
- });
426
- await (0, session_store_1.updateSession)(sessionToken, {
427
- idpRefreshToken: '',
428
- idpRefreshTokenExpires: undefined,
429
- refreshTokenClearedAt: Date.now(),
430
- refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
431
- });
432
- }
433
- else {
434
- // Concurrent refresh already rotated the token — our 401 is stale. Return the
435
- // freshly-rotated token directly so the loser succeeds without a retry
436
- // (race-fix follow-up #1).
437
- discardToken = false;
438
- console.warn('[AUTH_REFRESH] Discard suppressed: refresh token already rotated by a concurrent refresh — returning the fresh token (race-safe).');
439
- if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
440
- return server_1.NextResponse.json({
441
- refreshed: true,
442
- reason: 'completed_by_concurrent_request',
443
- accessTokenExpires: postRefresh.idpAccessTokenExpires,
444
- hasRefreshToken: !!postRefresh.idpRefreshToken,
445
- });
446
- }
447
- }
411
+ console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: IDP signaled discard_token', {
412
+ reason: 'IDP_DISCARD_TOKEN',
413
+ errorCode,
414
+ userId,
415
+ sessionToken: sessionToken.substring(0, 8) + '...',
416
+ hadRefreshToken: !!currentSession.idpRefreshToken,
417
+ stack: new Error().stack
418
+ });
419
+ await (0, session_store_1.updateSession)(sessionToken, {
420
+ idpRefreshToken: '',
421
+ idpRefreshTokenExpires: undefined,
422
+ refreshTokenClearedAt: Date.now(),
423
+ refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
424
+ });
448
425
  }
449
426
  return server_1.NextResponse.json({
450
427
  error: errorMessage,
@@ -60,7 +60,6 @@ const magic_link_1 = require("better-auth/plugins/magic-link");
60
60
  const idp_client_config_1 = require("../lib/idp-client-config");
61
61
  const app_slug_1 = require("../lib/app-slug");
62
62
  const redis_1 = require("../lib/redis");
63
- const session_store_1 = require("../lib/session-store");
64
63
  /**
65
64
  * Build Better Auth social providers from IDP config.
66
65
  */
@@ -126,24 +125,16 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
126
125
  // Redis-backed session storage via secondaryStorage
127
126
  secondaryStorage: {
128
127
  get: async (key) => {
129
- try {
130
- return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
131
- }
132
- catch {
133
- return null;
134
- }
128
+ return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
135
129
  },
136
130
  set: async (key, value, ttl) => {
137
- try {
138
- const redis = (0, redis_1.getRedis)();
139
- if (ttl) {
140
- await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
141
- }
142
- else {
143
- await redis.setex(`ba:${appSlug}:${key}`, session_store_1.SESSION_TTL, value);
144
- }
131
+ const redis = (0, redis_1.getRedis)();
132
+ if (ttl) {
133
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
134
+ }
135
+ else {
136
+ await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
145
137
  }
146
- catch { /* Redis unavailable — cookie cache still works */ }
147
138
  },
148
139
  delete: async (key) => {
149
140
  try {
@@ -348,13 +339,13 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
348
339
  };
349
340
  // Store in BA Redis session (for decodeSession)
350
341
  baData.idpTokens = idpTokenData;
351
- await (0, redis_1.getRedis)().setex(baKey, session_store_1.SESSION_TTL, JSON.stringify(baData));
342
+ await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
352
343
  // Write to canonical session store so refresh handler and token lifecycle can find the tokens.
353
344
  // Key format: {sessionPrefix}{token} — same key that getSession() reads from.
354
345
  try {
355
346
  const { getSessionPrefix } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
356
347
  const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
357
- await (0, redis_1.getRedis)().setex(canonicalKey, session_store_1.SESSION_TTL, JSON.stringify({
348
+ await (0, redis_1.getRedis)().setex(canonicalKey, 7 * 24 * 60 * 60, JSON.stringify({
358
349
  ...idpTokenData,
359
350
  oauthProvider: provider,
360
351
  }));
@@ -814,7 +814,13 @@ export declare const authClient: {
814
814
  isPending: boolean;
815
815
  isRefetching: boolean;
816
816
  error: import("@better-fetch/fetch").BetterFetchError | null;
817
- refetch: (queryParams?: {
817
+ refetch: (queryParams
818
+ /**
819
+ * NextAuth-compatible signOut wrapper.
820
+ *
821
+ * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
822
+ */
823
+ ?: {
818
824
  query?: import("better-auth/types").SessionQueryParams;
819
825
  } | undefined) => Promise<void>;
820
826
  };
@@ -988,7 +994,13 @@ export declare const useSession: () => {
988
994
  isPending: boolean;
989
995
  isRefetching: boolean;
990
996
  error: import("@better-fetch/fetch").BetterFetchError | null;
991
- refetch: (queryParams?: {
997
+ refetch: (queryParams
998
+ /**
999
+ * NextAuth-compatible signOut wrapper.
1000
+ *
1001
+ * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
1002
+ */
1003
+ ?: {
992
1004
  query?: import("better-auth/types").SessionQueryParams;
993
1005
  } | undefined) => Promise<void>;
994
1006
  }, signIn: {
@@ -200,38 +200,15 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
200
200
  if (!idpResponse.ok) {
201
201
  const idpError = responseData?.error || {};
202
202
  const code = idpError.code || 'UNKNOWN_ERROR';
203
- let discardToken = idpResponse.status === 401 || idpError.discard_token === true;
203
+ const discardToken = idpResponse.status === 401 || idpError.discard_token === true;
204
204
  const retryable = idpResponse.status !== 401 && idpError.retryable === true;
205
205
  if (discardToken) {
206
- // Race-safe discard (CAS): only clear if the refresh token we used is STILL
207
- // current. A concurrent refresh may have already rotated it — our 401 is then
208
- // stale/expected, and wiping here would silently log out a healthy session
209
- // (the rotation-race wipe, QA 6873 site 1/2). Compare-and-skip if it changed;
210
- // a concurrent rotation is NOT a terminal condition.
211
- const postRefresh = await (0, session_store_1.getSession)(sessionToken);
212
- const tokenUnchanged = !!session.idpRefreshToken && postRefresh?.idpRefreshToken === session.idpRefreshToken;
213
- if (tokenUnchanged) {
214
- await (0, session_store_1.updateSession)(sessionToken, {
215
- idpRefreshToken: '',
216
- idpRefreshTokenExpires: undefined,
217
- refreshTokenClearedAt: Date.now(),
218
- refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
219
- });
220
- }
221
- else {
222
- // Concurrent refresh already rotated the token — our 401 is stale. Return the
223
- // freshly-rotated token directly so the loser succeeds without a retry round-trip
224
- // (race-fix follow-up #1).
225
- discardToken = false;
226
- if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
227
- return {
228
- ok: true,
229
- accessToken: postRefresh.idpAccessToken,
230
- accessTokenExpires: postRefresh.idpAccessTokenExpires,
231
- refreshed: true,
232
- };
233
- }
234
- }
206
+ await (0, session_store_1.updateSession)(sessionToken, {
207
+ idpRefreshToken: '',
208
+ idpRefreshTokenExpires: undefined,
209
+ refreshTokenClearedAt: Date.now(),
210
+ refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
211
+ });
235
212
  }
236
213
  return {
237
214
  ok: false,
@@ -9,7 +9,6 @@
9
9
  */
10
10
  import { SessionData } from '../models/SessionModel';
11
11
  export type { SessionData } from '../models/SessionModel';
12
- export declare const SESSION_TTL: number;
13
12
  export interface RefreshLockInfo {
14
13
  sessionToken: string;
15
14
  acquiredAt: number;
@@ -12,7 +12,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.SESSION_TTL = void 0;
16
15
  exports.generateSessionToken = generateSessionToken;
17
16
  exports.createSession = createSession;
18
17
  exports.getSession = getSession;
@@ -47,10 +46,7 @@ const getSessionVersionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}ve
47
46
  // Better Auth uses a different key format: ba:{appSlug}:{token}
48
47
  const getBetterAuthSessionKey = (token, appSlug) => `ba:${appSlug || 'app'}:${token}`;
49
48
  const REFRESH_LOCK_TTL = 60; // 60 seconds
50
- // 3 days in seconds (matches refresh token lifetime). EXPORTED as the single source
51
- // of session TTL — better-auth.ts derives its BA-session Redis TTL from this so the
52
- // two can never drift apart again (the 7d-vs-3d drift = the zombie-session window, QA 6952).
53
- exports.SESSION_TTL = 3 * 24 * 60 * 60;
49
+ const SESSION_TTL = 3 * 24 * 60 * 60; // 3 days in seconds (matches refresh token lifetime)
54
50
  /**
55
51
  * Generates a new session token.
56
52
  * @returns A new session token string.
@@ -70,8 +66,8 @@ async function createSession(data) {
70
66
  const versionKey = getSessionVersionKey(sessionToken);
71
67
  try {
72
68
  await redis_1.default.multi()
73
- .setex(key, exports.SESSION_TTL, JSON.stringify(data))
74
- .setex(versionKey, exports.SESSION_TTL, '1')
69
+ .setex(key, SESSION_TTL, JSON.stringify(data))
70
+ .setex(versionKey, SESSION_TTL, '1')
75
71
  .exec();
76
72
  }
77
73
  catch (error) {
@@ -152,7 +148,7 @@ async function getBetterAuthSession(sessionToken, appSlug) {
152
148
  */
153
149
  async function touchSession(token) {
154
150
  const key = getSessionKey(token);
155
- await redis_1.default.expire(key, exports.SESSION_TTL);
151
+ await redis_1.default.expire(key, SESSION_TTL);
156
152
  }
157
153
  /**
158
154
  * Retrieves a session along with a version identifier for optimistic locking.
@@ -218,9 +214,9 @@ async function setSession(sessionToken, data) {
218
214
  const key = getSessionKey(sessionToken);
219
215
  const versionKey = getSessionVersionKey(sessionToken);
220
216
  await redis_1.default.multi()
221
- .setex(key, exports.SESSION_TTL, JSON.stringify(data))
217
+ .setex(key, SESSION_TTL, JSON.stringify(data))
222
218
  .incr(versionKey)
223
- .expire(versionKey, exports.SESSION_TTL)
219
+ .expire(versionKey, SESSION_TTL)
224
220
  .exec();
225
221
  }
226
222
  /**
@@ -258,9 +254,9 @@ async function updateSession(sessionToken, updates) {
258
254
  const updatedSession = { ...currentSession, ...updates };
259
255
  // Write the entire updated session back to Redis with version increment
260
256
  await redis_1.default.multi()
261
- .setex(key, exports.SESSION_TTL, JSON.stringify(updatedSession))
257
+ .setex(key, SESSION_TTL, JSON.stringify(updatedSession))
262
258
  .incr(versionKey)
263
- .expire(versionKey, exports.SESSION_TTL)
259
+ .expire(versionKey, SESSION_TTL)
264
260
  .exec();
265
261
  return updatedSession;
266
262
  }
@@ -489,27 +485,27 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
489
485
  const lockKey = getRefreshLockKey(sessionToken);
490
486
  try {
491
487
  // Lua script for atomic lock validation and release
492
- const luaScript = `
493
- local lockKey = KEYS[1]
494
- local expectedRequestId = ARGV[1]
495
- local expectedVersion = ARGV[2]
496
-
497
- local lockData = redis.call('GET', lockKey)
498
- if not lockData then
499
- return 0 -- Lock doesn't exist
500
- end
501
-
502
- local lockInfo = cjson.decode(lockData)
503
- if lockInfo.acquiredBy == expectedRequestId then
504
- if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
505
- redis.call('DEL', lockKey)
506
- return 1 -- Successfully released
507
- else
508
- return -2 -- Version mismatch
509
- end
510
- else
511
- return -1 -- Wrong owner
512
- end
488
+ const luaScript = `
489
+ local lockKey = KEYS[1]
490
+ local expectedRequestId = ARGV[1]
491
+ local expectedVersion = ARGV[2]
492
+
493
+ local lockData = redis.call('GET', lockKey)
494
+ if not lockData then
495
+ return 0 -- Lock doesn't exist
496
+ end
497
+
498
+ local lockInfo = cjson.decode(lockData)
499
+ if lockInfo.acquiredBy == expectedRequestId then
500
+ if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
501
+ redis.call('DEL', lockKey)
502
+ return 1 -- Successfully released
503
+ else
504
+ return -2 -- Version mismatch
505
+ end
506
+ else
507
+ return -1 -- Wrong owner
508
+ end
513
509
  `;
514
510
  const result = await redis_1.default.eval(luaScript, 1, lockKey, requestId, lockVersion ? lockVersion.toString() : '');
515
511
  if (result === 1) {
@@ -213,25 +213,25 @@ async function performInitialization() {
213
213
  const connectionLine = isConnectionError
214
214
  ? '║ 🔌 CONNECTION REFUSED - IDP appears to be down ║\n║ ║\n'
215
215
  : '';
216
- console.error(`
217
- ╔══════════════════════════════════════════════════════════════╗
218
- ║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
219
- ║ ║
220
- ║ The app cannot start without a valid auth signing secret. ║
221
- ║ This should be fetched from IDP at startup. ║
222
- ║ ║
223
- ${connectionLine}║ Possible causes: ║
224
- ║ • IDP is not running or unreachable ║
225
- ║ • CLIENT_ID is not registered in IDP ║
226
- ║ • IDP_URL is incorrect ║
227
- ║ • Network connectivity issue ║
228
- ║ ║
229
- ║ Current config: ║
230
- ║ • IDP_URL: ${idpUrl}║
231
- ║ • CLIENT_ID: ${clientId}║
232
- ╚══════════════════════════════════════════════════════════════╝
233
-
234
- [STARTUP] Error: ${errorMsg}
216
+ console.error(`
217
+ ╔══════════════════════════════════════════════════════════════╗
218
+ ║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
219
+ ║ ║
220
+ ║ The app cannot start without a valid auth signing secret. ║
221
+ ║ This should be fetched from IDP at startup. ║
222
+ ║ ║
223
+ ${connectionLine}║ Possible causes: ║
224
+ ║ • IDP is not running or unreachable ║
225
+ ║ • CLIENT_ID is not registered in IDP ║
226
+ ║ • IDP_URL is incorrect ║
227
+ ║ • Network connectivity issue ║
228
+ ║ ║
229
+ ║ Current config: ║
230
+ ║ • IDP_URL: ${idpUrl}║
231
+ ║ • CLIENT_ID: ${clientId}║
232
+ ╚══════════════════════════════════════════════════════════════╝
233
+
234
+ [STARTUP] Error: ${errorMsg}
235
235
  `);
236
236
  // Re-throw so callers know initialization failed
237
237
  throw lastInitError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.1.10",
3
+ "version": "4.1.12",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,11 +10,12 @@
10
10
  "src"
11
11
  ],
12
12
  "scripts": {
13
- "build": "npx tsc --project tsconfig.json",
13
+ "build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && npx tsc --project tsconfig.json",
14
14
  "prepublishOnly": "npm run build",
15
15
  "dev": "next dev",
16
16
  "dev:local": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-local.ps1",
17
- "dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1"
17
+ "dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1",
18
+ "prepack": "npm run build"
18
19
  },
19
20
  "exports": {
20
21
  ".": {
@@ -1,73 +1,73 @@
1
- #!/bin/bash
2
- #
3
- # CI Check: Detect incorrect internal API URL patterns
4
- #
5
- # This script fails if code is using req.url or req.nextUrl.origin to construct
6
- # URLs for internal API calls. These patterns cause 403 errors in Kubernetes
7
- # because they use the external HTTPS URL instead of INTERNAL_API_URL.
8
- #
9
- # CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
10
- # INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
11
- #
12
- # NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
13
- # The problem is when you construct a NEW path using req.url as the base.
14
- #
15
-
16
- set -e
17
-
18
- SEARCH_PATH="${1:-.}"
19
- ERRORS_FOUND=0
20
-
21
- echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
22
- echo "========================================================================"
23
-
24
- # Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
25
- # This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
26
- # But NOT: new URL(req.url) which is just parsing the request
27
- echo ""
28
- echo "Checking for: new URL('/api/...', req.url) pattern"
29
- if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
30
- echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
31
- ERRORS_FOUND=1
32
- fi
33
-
34
- # Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
35
- echo ""
36
- echo "Checking for: new URL(..., req.nextUrl) pattern"
37
- if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
38
- echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
39
- ERRORS_FOUND=1
40
- fi
41
-
42
- # Pattern 3: fetch with req.nextUrl.origin and /api/
43
- echo ""
44
- echo "Checking for: fetch with req.nextUrl.origin and /api/"
45
- if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
46
- echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
47
- ERRORS_FOUND=1
48
- fi
49
-
50
- # Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
51
- echo ""
52
- echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
53
- if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
54
- echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
55
- # Not failing on this one as it might be intentional in some cases (client-side refresh)
56
- fi
57
-
58
- echo ""
59
- echo "========================================================================"
60
-
61
- if [ $ERRORS_FOUND -eq 1 ]; then
62
- echo "FAILED: Dangerous internal URL patterns detected!"
63
- echo ""
64
- echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
65
- echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
66
- echo ""
67
- echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
68
- echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
69
- exit 1
70
- else
71
- echo "PASSED: No dangerous internal URL patterns found."
72
- exit 0
73
- fi
1
+ #!/bin/bash
2
+ #
3
+ # CI Check: Detect incorrect internal API URL patterns
4
+ #
5
+ # This script fails if code is using req.url or req.nextUrl.origin to construct
6
+ # URLs for internal API calls. These patterns cause 403 errors in Kubernetes
7
+ # because they use the external HTTPS URL instead of INTERNAL_API_URL.
8
+ #
9
+ # CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
10
+ # INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
11
+ #
12
+ # NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
13
+ # The problem is when you construct a NEW path using req.url as the base.
14
+ #
15
+
16
+ set -e
17
+
18
+ SEARCH_PATH="${1:-.}"
19
+ ERRORS_FOUND=0
20
+
21
+ echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
22
+ echo "========================================================================"
23
+
24
+ # Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
25
+ # This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
26
+ # But NOT: new URL(req.url) which is just parsing the request
27
+ echo ""
28
+ echo "Checking for: new URL('/api/...', req.url) pattern"
29
+ if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
30
+ echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
31
+ ERRORS_FOUND=1
32
+ fi
33
+
34
+ # Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
35
+ echo ""
36
+ echo "Checking for: new URL(..., req.nextUrl) pattern"
37
+ if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
38
+ echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
39
+ ERRORS_FOUND=1
40
+ fi
41
+
42
+ # Pattern 3: fetch with req.nextUrl.origin and /api/
43
+ echo ""
44
+ echo "Checking for: fetch with req.nextUrl.origin and /api/"
45
+ if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
46
+ echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
47
+ ERRORS_FOUND=1
48
+ fi
49
+
50
+ # Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
51
+ echo ""
52
+ echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
53
+ if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
54
+ echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
55
+ # Not failing on this one as it might be intentional in some cases (client-side refresh)
56
+ fi
57
+
58
+ echo ""
59
+ echo "========================================================================"
60
+
61
+ if [ $ERRORS_FOUND -eq 1 ]; then
62
+ echo "FAILED: Dangerous internal URL patterns detected!"
63
+ echo ""
64
+ echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
65
+ echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
66
+ echo ""
67
+ echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
68
+ echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
69
+ exit 1
70
+ else
71
+ echo "PASSED: No dangerous internal URL patterns found."
72
+ exit 0
73
+ fi
package/src/api/index.ts CHANGED
@@ -1,18 +1,18 @@
1
- /**
2
- * @payez/next-mvp API Module Exports
3
- *
4
- * Provides enhanced API route handlers with automatic token management
5
- *
6
- * @version 2.0.0
7
- * @since auth-ready-v2
8
- */
9
-
10
- export {
11
- createAuthHandler,
12
- type AuthContext,
13
- type AuthHandlerOptions,
14
- type HandlerFunction
15
- } from './auth-handler';
16
-
17
- // Default export for convenience
1
+ /**
2
+ * @payez/next-mvp API Module Exports
3
+ *
4
+ * Provides enhanced API route handlers with automatic token management
5
+ *
6
+ * @version 2.0.0
7
+ * @since auth-ready-v2
8
+ */
9
+
10
+ export {
11
+ createAuthHandler,
12
+ type AuthContext,
13
+ type AuthHandlerOptions,
14
+ type HandlerFunction
15
+ } from './auth-handler';
16
+
17
+ // Default export for convenience
18
18
  export { default } from './auth-handler';