@payez/next-mvp 3.2.1 → 3.2.3

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.
@@ -96,5 +96,7 @@ export interface MvpMiddlewareOptions {
96
96
  onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
97
97
  /** Additional paths to bypass middleware (beyond /api/auth/ and /api/session/) */
98
98
  bypassPaths?: string[];
99
+ /** Paths exempt from RBAC checks (auth still enforced, just no page-permission check) */
100
+ rbacExemptPaths?: string[];
99
101
  }
100
102
  export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
@@ -232,6 +232,7 @@ function createMvpMiddleware(options = {}) {
232
232
  const viabilityEndpoint = options.viabilityEndpoint || '/api/session/viability';
233
233
  const refreshEndpoint = options.refreshEndpoint || '/api/auth/refresh';
234
234
  const bypassPaths = options.bypassPaths || [];
235
+ const rbacExemptPaths = options.rbacExemptPaths || [];
235
236
  return async function middleware(request) {
236
237
  const { pathname, searchParams } = request.nextUrl;
237
238
  // =========================================================================
@@ -287,6 +288,7 @@ function createMvpMiddleware(options = {}) {
287
288
  circuitBreaker: cb,
288
289
  logger: log,
289
290
  refreshEndpoint,
291
+ rbacExemptPaths,
290
292
  onRefreshSuccess: options.onRefreshSuccess,
291
293
  onRefreshFailure: options.onRefreshFailure,
292
294
  });
@@ -317,7 +319,8 @@ async function checkViability(request, endpoint, log) {
317
319
  'Cache-Control': 'no-store',
318
320
  'Cookie': request.headers.get('cookie') || ''
319
321
  },
320
- credentials: 'include'
322
+ credentials: 'include',
323
+ signal: AbortSignal.timeout(5000),
321
324
  });
322
325
  if (response.ok) {
323
326
  const data = await response.json();
@@ -350,7 +353,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
350
353
  const safeCallback = getSafeCallbackUrl(pathname);
351
354
  switch (decision.type) {
352
355
  case 'allow':
353
- return handleAllow(request, pathname, sessionPointer, sessionStatus);
356
+ return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
354
357
  case 'redirect':
355
358
  return redirectTo(request, decision.location, decision.clearCookies);
356
359
  case 'service_error':
@@ -362,16 +365,20 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
362
365
  /** Paths that must never be RBAC-checked (they are RBAC redirect targets) */
363
366
  const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
364
367
  /** Handle 'allow' decision - run RBAC if enabled */
365
- async function handleAllow(request, pathname, sessionPointer, sessionStatus) {
368
+ async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
366
369
  const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
367
- if ((0, rbac_check_1.isRBACEnabled)() && !isPublic) {
368
- // Skip RBAC for error/fallback pages to prevent redirect loops
369
- if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))) {
370
+ if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists) {
371
+ // Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
372
+ if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
373
+ rbacExemptPaths.some(p => pathname.startsWith(p))) {
370
374
  return server_1.NextResponse.next();
371
375
  }
372
376
  if (!sessionPointer.clientId) {
373
- console.error('[MIDDLEWARE] RBAC: No clientId');
374
- return server_1.NextResponse.redirect(new URL('/error?code=no_client_id', request.url));
377
+ console.error('[MIDDLEWARE] RBAC: No clientId — returning 401');
378
+ if (pathname.startsWith('/api/')) {
379
+ return server_1.NextResponse.json({ error: 'Unauthorized — missing clientId for RBAC' }, { status: 401 });
380
+ }
381
+ return server_1.NextResponse.redirect(new URL('/unauthorized', request.url));
375
382
  }
376
383
  try {
377
384
  const result = await (0, rbac_check_1.checkPagePermission)(pathname, sessionPointer.roles, sessionPointer.clientId);
@@ -419,7 +426,8 @@ async function handleRefresh(request, safeCallback, opts) {
419
426
  'x-session-token': request.cookies.get((0, app_slug_1.getSessionCookieName)())?.value ||
420
427
  request.cookies.get((0, app_slug_1.getSecureSessionCookieName)())?.value || ''
421
428
  },
422
- credentials: 'include'
429
+ credentials: 'include',
430
+ signal: AbortSignal.timeout(5000),
423
431
  });
424
432
  if (response.ok) {
425
433
  const data = await response.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -108,6 +108,8 @@ export interface MvpMiddlewareOptions {
108
108
  onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
109
109
  /** Additional paths to bypass middleware (beyond /api/auth/ and /api/session/) */
110
110
  bypassPaths?: string[];
111
+ /** Paths exempt from RBAC checks (auth still enforced, just no page-permission check) */
112
+ rbacExemptPaths?: string[];
111
113
  }
112
114
 
113
115
  // =============================================================================
@@ -336,6 +338,7 @@ export function createMvpMiddleware(options: MvpMiddlewareOptions = {}) {
336
338
  const viabilityEndpoint = options.viabilityEndpoint || '/api/session/viability';
337
339
  const refreshEndpoint = options.refreshEndpoint || '/api/auth/refresh';
338
340
  const bypassPaths = options.bypassPaths || [];
341
+ const rbacExemptPaths = options.rbacExemptPaths || [];
339
342
 
340
343
  return async function middleware(request: NextRequest): Promise<NextResponse> {
341
344
  const { pathname, searchParams } = request.nextUrl;
@@ -400,6 +403,7 @@ export function createMvpMiddleware(options: MvpMiddlewareOptions = {}) {
400
403
  circuitBreaker: cb,
401
404
  logger: log,
402
405
  refreshEndpoint,
406
+ rbacExemptPaths,
403
407
  onRefreshSuccess: options.onRefreshSuccess,
404
408
  onRefreshFailure: options.onRefreshFailure,
405
409
  });
@@ -439,7 +443,8 @@ async function checkViability(
439
443
  'Cache-Control': 'no-store',
440
444
  'Cookie': request.headers.get('cookie') || ''
441
445
  },
442
- credentials: 'include'
446
+ credentials: 'include',
447
+ signal: AbortSignal.timeout(5000),
443
448
  });
444
449
 
445
450
  if (response.ok) {
@@ -473,6 +478,7 @@ interface ExecuteOptions {
473
478
  circuitBreaker: CircuitBreakerProvider;
474
479
  logger: MiddlewareLogger;
475
480
  refreshEndpoint: string;
481
+ rbacExemptPaths: string[];
476
482
  onRefreshSuccess?: () => void;
477
483
  onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
478
484
  }
@@ -490,7 +496,7 @@ async function executeDecision(
490
496
 
491
497
  switch (decision.type) {
492
498
  case 'allow':
493
- return handleAllow(request, pathname, sessionPointer, sessionStatus);
499
+ return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
494
500
 
495
501
  case 'redirect':
496
502
  return redirectTo(request, decision.location, decision.clearCookies);
@@ -511,19 +517,27 @@ async function handleAllow(
511
517
  request: NextRequest,
512
518
  pathname: string,
513
519
  sessionPointer: SessionPointer,
514
- sessionStatus: SessionStatus
520
+ sessionStatus: SessionStatus,
521
+ rbacExemptPaths: string[] = []
515
522
  ): Promise<NextResponse> {
516
523
  const isPublic = isUnauthenticatedRoute(pathname);
517
524
 
518
- if (isRBACEnabled() && !isPublic) {
519
- // Skip RBAC for error/fallback pages to prevent redirect loops
520
- if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))) {
525
+ if (isRBACEnabled() && !isPublic && sessionPointer.exists) {
526
+ // Skip RBAC for error/fallback pages (prevent redirect loops) and app-configured exempt paths
527
+ if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p)) ||
528
+ rbacExemptPaths.some(p => pathname.startsWith(p))) {
521
529
  return NextResponse.next();
522
530
  }
523
531
 
524
532
  if (!sessionPointer.clientId) {
525
- console.error('[MIDDLEWARE] RBAC: No clientId');
526
- return NextResponse.redirect(new URL('/error?code=no_client_id', request.url));
533
+ console.error('[MIDDLEWARE] RBAC: No clientId — returning 401');
534
+ if (pathname.startsWith('/api/')) {
535
+ return NextResponse.json(
536
+ { error: 'Unauthorized — missing clientId for RBAC' },
537
+ { status: 401 }
538
+ );
539
+ }
540
+ return NextResponse.redirect(new URL('/unauthorized', request.url));
527
541
  }
528
542
 
529
543
  try {
@@ -591,7 +605,8 @@ async function handleRefresh(
591
605
  'x-session-token': request.cookies.get(getSessionCookieName())?.value ||
592
606
  request.cookies.get(getSecureSessionCookieName())?.value || ''
593
607
  },
594
- credentials: 'include'
608
+ credentials: 'include',
609
+ signal: AbortSignal.timeout(5000),
595
610
  });
596
611
 
597
612
  if (response.ok) {