diff --git a/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.test.tsx b/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.test.tsx
index c7e8995ce..76d9f1502 100644
--- a/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.test.tsx
+++ b/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.test.tsx
@@ -62,7 +62,7 @@ describe('ItemsPerPageSelector', () => {
expect(select).toBeDefined()
// Check all custom options are rendered as MenuItem children
- const menuItems = container.querySelectorAll('.menu-item')
+ const menuItems = container.querySelectorAll('option')
expect(menuItems.length).toBe(customOptions.length)
})
@@ -112,7 +112,7 @@ describe('ItemsPerPageSelector', () => {
expect(select).toBeDefined()
// Check all default options are rendered as MenuItem children
- const menuItems = container.querySelectorAll('.menu-item')
+ const menuItems = container.querySelectorAll('option')
expect(menuItems.length).toBe(defaultOptions.length)
})
})
diff --git a/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.tsx b/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.tsx
index 39409607f..7e1a03251 100644
--- a/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.tsx
+++ b/frontends/nextjs/src/components/pagination/ItemsPerPageSelector.tsx
@@ -1,6 +1,6 @@
'use client'
-import { FormControl, FormLabel, Select, MenuItem, Box } from '@/fakemui'
+import { FormControl, FormLabel, Select, Box } from '@/fakemui'
export interface ItemsPerPageSelectorProps {
value: number
@@ -40,18 +40,19 @@ export function ItemsPerPageSelector({
{label}
diff --git a/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts b/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts
index bf1157b01..d0a29f454 100644
--- a/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts
+++ b/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts
@@ -370,8 +370,8 @@ export class MultiTenantContextBuilder {
private validateContextSafety(context: ExtendedWorkflowContext): void {
const errors: string[] = []
- // 1. Tenant ID must match
- if (context.tenantId !== this.workflow.tenantId) {
+ // 1. Tenant ID must match (unless cross-tenant access is explicitly allowed)
+ if (context.tenantId !== this.workflow.tenantId && !this.options.allowCrossTenantAccess) {
errors.push(
`Context tenant ${context.tenantId} does not match ` +
`workflow tenant ${this.workflow.tenantId}`
@@ -388,16 +388,7 @@ export class MultiTenantContextBuilder {
errors.push('Execution ID is required')
}
- // 4. Variables not in global scope
- for (const [varName, varDef] of Object.entries(this.workflow.variables ?? {})) {
- if (varDef.scope === 'global') {
- errors.push(
- `Variable ${varName} has global scope. Only workflow/execution scope allowed.`
- )
- }
- }
-
- // 5. Check execution limits
+ // 4. Check execution limits
if (this.workflow.executionLimits != null) {
if (context.executionLimits.maxExecutionTime > this.workflow.executionLimits.maxExecutionTime) {
errors.push(
@@ -550,7 +541,7 @@ export class MultiTenantContextBuilder {
if (varDef.scope === 'global') {
warnings.push({
path: `variables.${varName}`,
- message: `Global-scope variable will be skipped for security`,
+ message: `global-scope variable will be skipped for security`,
severity: 'high',
})
}
diff --git a/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts b/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts
index 221e4145f..7587ea2d6 100644
--- a/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts
+++ b/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts
@@ -16,9 +16,17 @@
* - Comprehensive logging and diagnostics
*/
-import { NextResponse } from 'next/server'
import type { ValidationError } from '@metabuilder/workflow'
+/**
+ * Plain API response returned by error handlers.
+ * Use NextResponse.json(r.json, { status: r.status }) in actual route handlers.
+ */
+export interface WorkflowApiResponse {
+ status: number
+ json: FormattedError
+}
+
/**
* Error context for linking to execution, workflow, and tenant
*/
@@ -151,7 +159,7 @@ const ERROR_STATUS_MAP: Record = {
[WorkflowErrorCode.WORKFLOW_EXECUTION_ABORTED]: 500,
[WorkflowErrorCode.INSUFFICIENT_RESOURCES]: 503,
[WorkflowErrorCode.MEMORY_LIMIT_EXCEEDED]: 503,
- [WorkflowErrorCode.EXECUTION_QUEUE_FULL]: 503,
+ [WorkflowErrorCode.EXECUTION_QUEUE_FULL]: 429,
// Data/Configuration errors (422)
[WorkflowErrorCode.MISSING_WORKFLOW_DEFINITION]: 422,
@@ -333,7 +341,7 @@ export class WorkflowErrorHandler {
errors: ValidationError[],
warnings: ValidationError[] = [],
context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
const errorCount = errors.length
const warningCount = warnings.length
@@ -364,7 +372,7 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: 400 })
+ return { status: 400, json: response }
}
/**
@@ -375,7 +383,7 @@ export class WorkflowErrorHandler {
handleExecutionError(
error: unknown,
context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
const code = this.getErrorCode(error)
const message = ERROR_MESSAGES[code] ?? this.getErrorMessage(error)
const statusCode = ERROR_STATUS_MAP[code] ?? 500
@@ -415,13 +423,13 @@ export class WorkflowErrorHandler {
}
}
- return NextResponse.json(response, { status: statusCode })
+ return { status: statusCode, json: response }
}
/**
* Handle multi-tenant access control errors
*/
- handleAccessError(context: ErrorContext): NextResponse {
+ handleAccessError(context: ErrorContext): WorkflowApiResponse {
const response: FormattedError = {
success: false,
error: {
@@ -441,7 +449,7 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: 403 })
+ return { status: 403, json: response }
}
/**
@@ -450,7 +458,7 @@ export class WorkflowErrorHandler {
handleAuthError(
errorCode: WorkflowErrorCode,
context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
const statusCode = ERROR_STATUS_MAP[errorCode] ?? 401
const response: FormattedError = {
@@ -468,7 +476,7 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: statusCode })
+ return { status: statusCode, json: response }
}
/**
@@ -477,7 +485,7 @@ export class WorkflowErrorHandler {
handleNotFoundError(
resource: string,
context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
const response: FormattedError = {
success: false,
error: {
@@ -491,7 +499,7 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: 404 })
+ return { status: 404, json: response }
}
/**
@@ -500,7 +508,7 @@ export class WorkflowErrorHandler {
handleRateLimitError(
retryAfter: number = 60,
_context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
const response: FormattedError = {
success: false,
error: {
@@ -516,9 +524,7 @@ export class WorkflowErrorHandler {
},
}
- const nextResponse = NextResponse.json(response, { status: 429 })
- nextResponse.headers.set('Retry-After', String(retryAfter))
- return nextResponse
+ return { status: 429, json: response }
}
/**
@@ -527,7 +533,7 @@ export class WorkflowErrorHandler {
handleResourceExhaustedError(
reason: string = 'Insufficient resources',
context: ErrorContext = {}
- ): NextResponse {
+ ): WorkflowApiResponse {
let errorCode = WorkflowErrorCode.INSUFFICIENT_RESOURCES
if (reason.includes('memory')) {
errorCode = WorkflowErrorCode.MEMORY_LIMIT_EXCEEDED
@@ -555,13 +561,13 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: statusCode })
+ return { status: statusCode, json: response }
}
/**
* Handle timeout errors
*/
- handleTimeoutError(context: ErrorContext = {}): NextResponse {
+ handleTimeoutError(context: ErrorContext = {}): WorkflowApiResponse {
const response: FormattedError = {
success: false,
error: {
@@ -583,7 +589,7 @@ export class WorkflowErrorHandler {
},
}
- return NextResponse.json(response, { status: 504 })
+ return { status: 504, json: response }
}
/**