@payez/next-mvp 3.2.2 → 3.3.0

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.
@@ -81,9 +81,9 @@ async function vibeServiceRequest(endpoint, options) {
81
81
  .update(stringToSign)
82
82
  .digest('base64');
83
83
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
84
- // Get the numeric client ID from startup config for multi-client admin support
84
+ // Get the client slug from startup config for multi-client admin support
85
85
  const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
86
- const numericClientId = idpConfig?.clientId;
86
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
87
87
  try {
88
88
  const res = await fetch(proxyUrl, {
89
89
  method: 'POST',
@@ -92,7 +92,7 @@ async function vibeServiceRequest(endpoint, options) {
92
92
  'X-Vibe-Client-Id': clientId,
93
93
  'X-Vibe-Timestamp': String(timestamp),
94
94
  'X-Vibe-Signature': signature,
95
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
95
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
96
96
  },
97
97
  body: JSON.stringify({
98
98
  endpoint,
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
80
80
  .update(stringToSign)
81
81
  .digest('base64');
82
82
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
83
- // Get the numeric client ID from startup config for multi-client admin support
83
+ // Get the client slug from startup config for multi-client admin support
84
84
  const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
85
- const numericClientId = idpConfig?.clientId;
85
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
86
86
  try {
87
87
  const res = await fetch(proxyUrl, {
88
88
  method: 'POST',
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
91
91
  'X-Vibe-Client-Id': clientId,
92
92
  'X-Vibe-Timestamp': String(timestamp),
93
93
  'X-Vibe-Signature': signature,
94
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
94
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
95
95
  },
96
96
  body: JSON.stringify({
97
97
  endpoint,
@@ -87,9 +87,9 @@ async function vibeServiceRequest(endpoint, options) {
87
87
  .update(stringToSign)
88
88
  .digest('base64');
89
89
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
90
- // Get the numeric client ID from startup config for multi-client admin support
90
+ // Get the client slug from startup config for multi-client admin support
91
91
  const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
92
- const numericClientId = idpConfig?.clientId;
92
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
93
93
  try {
94
94
  const res = await fetch(proxyUrl, {
95
95
  method: 'POST',
@@ -98,7 +98,7 @@ async function vibeServiceRequest(endpoint, options) {
98
98
  'X-Vibe-Client-Id': clientId,
99
99
  'X-Vibe-Timestamp': String(timestamp),
100
100
  'X-Vibe-Signature': signature,
101
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
101
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
102
102
  },
103
103
  body: JSON.stringify({
104
104
  endpoint,
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
80
80
  .update(stringToSign)
81
81
  .digest('base64');
82
82
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
83
- // Get the numeric client ID from startup config for multi-client admin support
83
+ // Get the client slug from startup config for multi-client admin support
84
84
  const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
85
- const numericClientId = idpConfig?.clientId;
85
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
86
86
  try {
87
87
  const res = await fetch(proxyUrl, {
88
88
  method: 'POST',
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
91
91
  'X-Vibe-Client-Id': clientId,
92
92
  'X-Vibe-Timestamp': String(timestamp),
93
93
  'X-Vibe-Signature': signature,
94
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
94
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
95
95
  },
96
96
  body: JSON.stringify({
97
97
  endpoint,
@@ -95,9 +95,9 @@ async function vibeServiceRequest(endpoint, options) {
95
95
  .update(stringToSign)
96
96
  .digest('base64');
97
97
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
98
- // Get the numeric client ID from startup config for multi-client admin support
98
+ // Get the client slug from startup config for multi-client admin support
99
99
  const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
100
- const numericClientId = idpConfig?.clientId;
100
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
101
101
  try {
102
102
  const res = await fetch(proxyUrl, {
103
103
  method: 'POST',
@@ -107,7 +107,7 @@ async function vibeServiceRequest(endpoint, options) {
107
107
  'X-Vibe-Timestamp': String(timestamp),
108
108
  'X-Vibe-Signature': signature,
109
109
  // For multi-client admins: specify which client context to use
110
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
110
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
111
111
  },
112
112
  body: JSON.stringify({
113
113
  endpoint,
@@ -45,7 +45,7 @@ export interface BrandingConfig {
45
45
  logoUrl?: string;
46
46
  }
47
47
  export interface IDPClientConfig {
48
- clientId: number;
48
+ clientId: string;
49
49
  clientSlug: string;
50
50
  nextAuthSecret: string;
51
51
  configCacheTtlSeconds: number;
@@ -282,7 +282,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
282
282
  }
283
283
  // Map response to our interface (IDP always returns snake_case)
284
284
  const config = {
285
- clientId: typeof rawClientId === 'string' ? parseInt(rawClientId, 10) : rawClientId,
285
+ clientId: String(rawClientId),
286
286
  clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
287
287
  nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
288
288
  configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
@@ -330,7 +330,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
330
330
  console.log(`[IDP_CONFIG] Parsed baseClientUrl:`, config.baseClientUrl, `| raw keys:`, Object.keys(configData).filter(k => k.toLowerCase().includes('client')));
331
331
  // Validate we got what we need
332
332
  if (!config.clientId) {
333
- throw new Error('[IDP_CONFIG] FATAL: clientId is 0 or missing after parsing');
333
+ throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
334
334
  }
335
335
  if (!config.nextAuthSecret) {
336
336
  throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
@@ -31,9 +31,6 @@ async function resolveNextAuthSecret() {
31
31
  const clientIdStr = process.env.CLIENT_ID;
32
32
  if (!clientIdStr || clientIdStr.trim() === '')
33
33
  throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
34
- // Determine if clientId is numeric or string
35
- const isNumeric = /^[0-9]+$/.test(clientIdStr);
36
- const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
37
34
  // Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
38
35
  const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
39
36
  // Client ID passed via X-Client-Id header, not query string
@@ -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
  });
@@ -351,7 +353,7 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
351
353
  const safeCallback = getSafeCallbackUrl(pathname);
352
354
  switch (decision.type) {
353
355
  case 'allow':
354
- return handleAllow(request, pathname, sessionPointer, sessionStatus);
356
+ return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
355
357
  case 'redirect':
356
358
  return redirectTo(request, decision.location, decision.clearCookies);
357
359
  case 'service_error':
@@ -363,11 +365,12 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
363
365
  /** Paths that must never be RBAC-checked (they are RBAC redirect targets) */
364
366
  const RBAC_EXEMPT_PATHS = ['/error', '/unauthorized', '/service-unavailable'];
365
367
  /** Handle 'allow' decision - run RBAC if enabled */
366
- async function handleAllow(request, pathname, sessionPointer, sessionStatus) {
368
+ async function handleAllow(request, pathname, sessionPointer, sessionStatus, rbacExemptPaths = []) {
367
369
  const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
368
370
  if ((0, rbac_check_1.isRBACEnabled)() && !isPublic && sessionPointer.exists) {
369
- // Skip RBAC for error/fallback pages to prevent redirect loops
370
- if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))) {
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))) {
371
374
  return server_1.NextResponse.next();
372
375
  }
373
376
  if (!sessionPointer.clientId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,9 +65,9 @@ async function vibeServiceRequest<T = unknown>(
65
65
 
66
66
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
67
67
 
68
- // Get the numeric client ID from startup config for multi-client admin support
68
+ // Get the client slug from startup config for multi-client admin support
69
69
  const idpConfig = getStartupIDPConfig();
70
- const numericClientId = idpConfig?.clientId;
70
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
71
71
 
72
72
  try {
73
73
  const res = await fetch(proxyUrl, {
@@ -77,7 +77,7 @@ async function vibeServiceRequest<T = unknown>(
77
77
  'X-Vibe-Client-Id': clientId,
78
78
  'X-Vibe-Timestamp': String(timestamp),
79
79
  'X-Vibe-Signature': signature,
80
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
80
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
81
81
  },
82
82
  body: JSON.stringify({
83
83
  endpoint,
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
64
64
 
65
65
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
66
66
 
67
- // Get the numeric client ID from startup config for multi-client admin support
67
+ // Get the client slug from startup config for multi-client admin support
68
68
  const idpConfig = getStartupIDPConfig();
69
- const numericClientId = idpConfig?.clientId;
69
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
70
70
 
71
71
  try {
72
72
  const res = await fetch(proxyUrl, {
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
76
76
  'X-Vibe-Client-Id': clientId,
77
77
  'X-Vibe-Timestamp': String(timestamp),
78
78
  'X-Vibe-Signature': signature,
79
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
79
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
80
80
  },
81
81
  body: JSON.stringify({
82
82
  endpoint,
@@ -77,9 +77,9 @@ async function vibeServiceRequest<T = unknown>(
77
77
 
78
78
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
79
79
 
80
- // Get the numeric client ID from startup config for multi-client admin support
80
+ // Get the client slug from startup config for multi-client admin support
81
81
  const idpConfig = getStartupIDPConfig();
82
- const numericClientId = idpConfig?.clientId;
82
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
83
83
 
84
84
  try {
85
85
  const res = await fetch(proxyUrl, {
@@ -89,7 +89,7 @@ async function vibeServiceRequest<T = unknown>(
89
89
  'X-Vibe-Client-Id': clientId,
90
90
  'X-Vibe-Timestamp': String(timestamp),
91
91
  'X-Vibe-Signature': signature,
92
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
92
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
93
93
  },
94
94
  body: JSON.stringify({
95
95
  endpoint,
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
64
64
 
65
65
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
66
66
 
67
- // Get the numeric client ID from startup config for multi-client admin support
67
+ // Get the client slug from startup config for multi-client admin support
68
68
  const idpConfig = getStartupIDPConfig();
69
- const numericClientId = idpConfig?.clientId;
69
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
70
70
 
71
71
  try {
72
72
  const res = await fetch(proxyUrl, {
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
76
76
  'X-Vibe-Client-Id': clientId,
77
77
  'X-Vibe-Timestamp': String(timestamp),
78
78
  'X-Vibe-Signature': signature,
79
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
79
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
80
80
  },
81
81
  body: JSON.stringify({
82
82
  endpoint,
@@ -79,9 +79,9 @@ async function vibeServiceRequest<T = unknown>(
79
79
 
80
80
  const proxyUrl = `${idpUrl}/api/vibe/proxy`;
81
81
 
82
- // Get the numeric client ID from startup config for multi-client admin support
82
+ // Get the client slug from startup config for multi-client admin support
83
83
  const idpConfig = getStartupIDPConfig();
84
- const numericClientId = idpConfig?.clientId;
84
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
85
85
 
86
86
  try {
87
87
  const res = await fetch(proxyUrl, {
@@ -92,7 +92,7 @@ async function vibeServiceRequest<T = unknown>(
92
92
  'X-Vibe-Timestamp': String(timestamp),
93
93
  'X-Vibe-Signature': signature,
94
94
  // For multi-client admins: specify which client context to use
95
- ...(numericClientId && { 'X-Client-Id': String(numericClientId) }),
95
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
96
96
  },
97
97
  body: JSON.stringify({
98
98
  endpoint,
@@ -56,7 +56,7 @@ export interface BrandingConfig {
56
56
  }
57
57
 
58
58
  export interface IDPClientConfig {
59
- clientId: number;
59
+ clientId: string;
60
60
  clientSlug: string;
61
61
  nextAuthSecret: string;
62
62
  configCacheTtlSeconds: number;
@@ -371,7 +371,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
371
371
 
372
372
  // Map response to our interface (IDP always returns snake_case)
373
373
  const config: IDPClientConfig = {
374
- clientId: typeof rawClientId === 'string' ? parseInt(rawClientId, 10) : rawClientId,
374
+ clientId: String(rawClientId),
375
375
  clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
376
376
  nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
377
377
  configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
@@ -420,7 +420,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
420
420
 
421
421
  // Validate we got what we need
422
422
  if (!config.clientId) {
423
- throw new Error('[IDP_CONFIG] FATAL: clientId is 0 or missing after parsing');
423
+ throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
424
424
  }
425
425
  if (!config.nextAuthSecret) {
426
426
  throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
@@ -32,10 +32,6 @@ export async function resolveNextAuthSecret(): Promise<string> {
32
32
  const clientIdStr = process.env.CLIENT_ID;
33
33
  if (!clientIdStr || clientIdStr.trim() === '') throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
34
34
 
35
- // Determine if clientId is numeric or string
36
- const isNumeric = /^[0-9]+$/.test(clientIdStr);
37
- const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
38
-
39
35
  // Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
40
36
 
41
37
  const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
@@ -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
  });
@@ -474,6 +478,7 @@ interface ExecuteOptions {
474
478
  circuitBreaker: CircuitBreakerProvider;
475
479
  logger: MiddlewareLogger;
476
480
  refreshEndpoint: string;
481
+ rbacExemptPaths: string[];
477
482
  onRefreshSuccess?: () => void;
478
483
  onRefreshFailure?: (status: number, isNetworkError: boolean) => void;
479
484
  }
@@ -491,7 +496,7 @@ async function executeDecision(
491
496
 
492
497
  switch (decision.type) {
493
498
  case 'allow':
494
- return handleAllow(request, pathname, sessionPointer, sessionStatus);
499
+ return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
495
500
 
496
501
  case 'redirect':
497
502
  return redirectTo(request, decision.location, decision.clearCookies);
@@ -512,13 +517,15 @@ async function handleAllow(
512
517
  request: NextRequest,
513
518
  pathname: string,
514
519
  sessionPointer: SessionPointer,
515
- sessionStatus: SessionStatus
520
+ sessionStatus: SessionStatus,
521
+ rbacExemptPaths: string[] = []
516
522
  ): Promise<NextResponse> {
517
523
  const isPublic = isUnauthenticatedRoute(pathname);
518
524
 
519
525
  if (isRBACEnabled() && !isPublic && sessionPointer.exists) {
520
- // Skip RBAC for error/fallback pages to prevent redirect loops
521
- if (RBAC_EXEMPT_PATHS.some(p => pathname.startsWith(p))) {
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))) {
522
529
  return NextResponse.next();
523
530
  }
524
531