diff --git a/frontends/nextjs/next.config.ts b/frontends/nextjs/next.config.ts index 8c868580a..a5cddc37e 100644 --- a/frontends/nextjs/next.config.ts +++ b/frontends/nextjs/next.config.ts @@ -1,5 +1,6 @@ import type { NextConfig } from 'next' import type { Configuration } from 'webpack' +import type webpack from 'webpack' import path from 'path' const projectDir = process.cwd() @@ -101,12 +102,13 @@ const nextConfig: NextConfig = { '@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'), }, }, - webpack(config: Configuration, { isServer, webpack }) { + webpack(config: Configuration, { isServer, webpack: wp }: { isServer: boolean; webpack: typeof webpack }) { // Stub ALL external SCSS module imports with an actual .module.scss // so they go through the CSS module pipeline (css-loader sets .default correctly) const stubScss = path.resolve(projectDir, 'src/lib/empty.module.scss') - config.plugins!.push( - new webpack.NormalModuleReplacementPlugin( + config.plugins ??= [] + config.plugins.push( + new wp.NormalModuleReplacementPlugin( /\.module\.scss$/, function (resource: { context?: string; request?: string }) { const ctx = resource.context ?? '' @@ -116,30 +118,36 @@ const nextConfig: NextConfig = { } ) ) - config.optimization!.minimize = false - - config.resolve!.alias = { - ...(config.resolve!.alias as Record), - '@metabuilder/components': path.resolve(projectDir, 'src/lib/components-shim.ts'), - '@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'), - // Resolve service-adapters to source (dist/ is not pre-built) - '@metabuilder/service-adapters': path.resolve(monorepoRoot, 'redux/adapters/src'), + if (config.optimization != null) { + config.optimization.minimize = false } - config.externals = [...((config.externals as string[]) ?? []), 'esbuild'] + if (config.resolve != null) { + config.resolve.alias = { + ...(config.resolve.alias as Record), + '@metabuilder/components': path.resolve(projectDir, 'src/lib/components-shim.ts'), + '@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'), + // Resolve service-adapters to source (dist/ is not pre-built) + '@metabuilder/service-adapters': path.resolve(monorepoRoot, 'redux/adapters/src'), + } + } + + config.externals = [...(config.externals as string[]), 'esbuild'] if (!isServer) { - config.resolve!.fallback = { - ...(config.resolve!.fallback as Record), - '@aws-sdk/client-s3': false, - fs: false, - path: false, - crypto: false, - stream: false, - 'stream/promises': false, - os: false, - buffer: false, - util: false, + if (config.resolve != null) { + config.resolve.fallback = { + ...(config.resolve.fallback as Record), + '@aws-sdk/client-s3': false, + fs: false, + path: false, + crypto: false, + stream: false, + 'stream/promises': false, + os: false, + buffer: false, + util: false, + } } } diff --git a/frontends/nextjs/src/app/api/bootstrap/route.ts b/frontends/nextjs/src/app/api/bootstrap/route.ts index 7a86599c7..a1a46559c 100644 --- a/frontends/nextjs/src/app/api/bootstrap/route.ts +++ b/frontends/nextjs/src/app/api/bootstrap/route.ts @@ -70,7 +70,7 @@ async function dbalPost(entity: string, data: Record): Promise< export async function POST(request: NextRequest) { const limitResponse = applyRateLimit(request, 'bootstrap') - if (limitResponse !== null && limitResponse !== undefined) { + if (limitResponse != null) { return limitResponse } diff --git a/frontends/nextjs/src/app/api/v1/[tenant]/workflows/route.ts b/frontends/nextjs/src/app/api/v1/[tenant]/workflows/route.ts index 319120bb5..e026f6907 100644 --- a/frontends/nextjs/src/app/api/v1/[tenant]/workflows/route.ts +++ b/frontends/nextjs/src/app/api/v1/[tenant]/workflows/route.ts @@ -117,9 +117,8 @@ export async function GET( const offset = parseInt(searchParams.get('offset') ?? '0') const category = searchParams.get('category') const tags = searchParams.get('tags')?.split(',').map((t) => t.trim()) - const active = searchParams.get('active') - ? searchParams.get('active') === 'true' - : undefined + const activeParam = searchParams.get('active') + const active = activeParam != null ? activeParam === 'true' : undefined // 6. Build filter const filter: Record = { @@ -286,9 +285,9 @@ export async function POST( notificationChannels: [], }, nodes: Array.isArray(body.nodes) ? (body.nodes as unknown[]) : [], - connections: (body.connections as Record) ?? {}, + connections: (body.connections as Record), triggers: Array.isArray(body.triggers) ? (body.triggers as unknown[]) : [], - variables: (body.variables as Record) ?? {}, + variables: (body.variables as Record), errorHandling: { default: 'stopWorkflow' as const, errorNotification: false, @@ -309,7 +308,7 @@ export async function POST( onLimitExceeded: 'reject' as const, }, credentials: [], - metadata: (body.metadata as Record) ?? {}, + metadata: (body.metadata as Record), executionLimits: { maxExecutionTime: 300000, maxMemoryMb: 512, diff --git a/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx b/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx index 148bb0f82..05eed0c5f 100644 --- a/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx +++ b/frontends/nextjs/src/components/workflow/ExecutionMonitor.tsx @@ -121,7 +121,7 @@ export const ExecutionMonitor: React.FC = ({ key={execution.id} execution={execution} isSelected={selectedExecutionId === execution.id} - onClick={() => handleExecutionSelect(execution.id)} + onClick={() => { handleExecutionSelect(execution.id) }} /> ))} @@ -146,11 +146,11 @@ export const ExecutionMonitor: React.FC = ({ nodeId={nodeId} result={result} isExpanded={expandedNodeId === nodeId} - onToggle={() => + onToggle={() => { setExpandedNodeId( expandedNodeId === nodeId ? null : nodeId ) - } + }} /> ))} @@ -420,7 +420,7 @@ const LogViewer: React.FC = ({ logs }) => { diff --git a/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx b/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx index d70e9be0b..9669bb6b9 100644 --- a/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx +++ b/frontends/nextjs/src/components/workflow/WorkflowBuilder.tsx @@ -167,7 +167,7 @@ export const WorkflowBuilder: React.FC = ({ { handleTriggerDataChange(name, e.target.value) }} @@ -344,13 +344,13 @@ function renderConnections(workflow: WorkflowDefinition) { Object.entries(workflow.connections).forEach(([fromNodeId, portMap]: [string, Record>]) => { const fromNode = workflow.nodes.find((n) => n.id === fromNodeId) - if (!fromNode) return + if (fromNode == null) return Object.entries(portMap).forEach(([_portName, indexMap]: [string, Record]) => { Object.entries(indexMap).forEach(([_, targets]: [string, ConnectionTarget[]]) => { targets.forEach((target: ConnectionTarget) => { const toNode = workflow.nodes.find((n) => n.id === target.node) - if (!toNode) return + if (toNode == null) return const [x1, y1] = fromNode.position const [x2, y2] = toNode.position diff --git a/frontends/nextjs/src/lib/admin/package-utils.ts b/frontends/nextjs/src/lib/admin/package-utils.ts index 95257a3f0..673dd9bdf 100644 --- a/frontends/nextjs/src/lib/admin/package-utils.ts +++ b/frontends/nextjs/src/lib/admin/package-utils.ts @@ -82,7 +82,7 @@ export function getErrorMessage(error: PackageError | Error | null): string { return messages[error.code] ?? error.message } - return error.message ?? 'An unknown error occurred' + return error.message } /** @@ -135,7 +135,7 @@ export function formatVersion(version: string): string { // Ensure version matches semver format const semverRegex = /^\d+\.\d+\.\d+/ const match = version.match(semverRegex) - return match ? match[0] : version + return match != null ? match[0] : version } /** diff --git a/frontends/nextjs/src/lib/auth/api/fetch-session.ts b/frontends/nextjs/src/lib/auth/api/fetch-session.ts index 77725c258..1bc65a21a 100644 --- a/frontends/nextjs/src/lib/auth/api/fetch-session.ts +++ b/frontends/nextjs/src/lib/auth/api/fetch-session.ts @@ -28,7 +28,7 @@ export async function fetchSession(): Promise { filter: { token: sessionToken } }) - const session = sessions.data?.[0] as DbalSessionRecord | undefined + const session = sessions.data[0] as DbalSessionRecord | undefined if (session === undefined) { return null diff --git a/frontends/nextjs/src/lib/auth/api/login.ts b/frontends/nextjs/src/lib/auth/api/login.ts index 7b15b4dc7..7c8755910 100644 --- a/frontends/nextjs/src/lib/auth/api/login.ts +++ b/frontends/nextjs/src/lib/auth/api/login.ts @@ -45,7 +45,7 @@ export async function login(identifier: string, password: string): Promise { const cookieStore = await cookies() const sessionToken = cookieStore.get(SESSION_COOKIE) - if (!sessionToken?.value || sessionToken.value.length === 0) { + if (sessionToken?.value == null || sessionToken.value.length === 0) { return null } @@ -35,7 +35,7 @@ export async function getCurrentUser(): Promise { filter: { token: sessionToken.value } }) - const session = sessions.data?.[0] as DbalSessionRecord | undefined + const session = sessions.data[0] as DbalSessionRecord | undefined if (session == null) { return null diff --git a/frontends/nextjs/src/lib/db-client.ts b/frontends/nextjs/src/lib/db-client.ts index 6772cdaaa..140f24e1c 100644 --- a/frontends/nextjs/src/lib/db-client.ts +++ b/frontends/nextjs/src/lib/db-client.ts @@ -109,7 +109,7 @@ function createOps(entityName: string): EntityOps { return { async list(options?: ListOptions): Promise { const params = new URLSearchParams() - if (options?.filter) { + if (options?.filter != null) { for (const [k, v] of Object.entries(options.filter)) { if (v !== undefined && v !== null) params.set(k, String(v as string | number | boolean)) } @@ -127,7 +127,7 @@ function createOps(entityName: string): EntityOps { if (Array.isArray(payload)) { return { data: payload, total: payload.length } } - if (payload != null && Array.isArray(payload.data)) { + if (Array.isArray(payload.data)) { return { data: payload.data as Record[], total: payload.total as number | undefined } } return { data: [] } diff --git a/frontends/nextjs/src/lib/error-reporting.ts b/frontends/nextjs/src/lib/error-reporting.ts index 08d0a734e..5e554789f 100644 --- a/frontends/nextjs/src/lib/error-reporting.ts +++ b/frontends/nextjs/src/lib/error-reporting.ts @@ -161,7 +161,7 @@ class ErrorReportingService { // suggestedAction reflects the current category, even if mutated after creation Object.defineProperty(report, 'suggestedAction', { - get() { return getSuggestedAction(this.category) }, + get(this: ErrorReport) { return getSuggestedAction(this.category) }, enumerable: true, configurable: true, }) diff --git a/frontends/nextjs/src/lib/workflow/multi-tenant-context.examples.ts b/frontends/nextjs/src/lib/workflow/multi-tenant-context.examples.ts index fae140063..a546f8a90 100644 --- a/frontends/nextjs/src/lib/workflow/multi-tenant-context.examples.ts +++ b/frontends/nextjs/src/lib/workflow/multi-tenant-context.examples.ts @@ -332,7 +332,7 @@ export async function executeScheduledWorkflow( }) // 2. Get schedule configuration - const trigger = workflow.triggers?.find((t) => t.kind === 'schedule') + const trigger = workflow.triggers.find((t) => t.kind === 'schedule') const cronExpression = trigger?.schedule ?? '0 */6 * * *' const context = await builder.build( @@ -430,7 +430,7 @@ export async function validateWorkflowExecution(req: NextRequest) { } const builder = new MultiTenantContextBuilder(workflow, requestContext) - const result = await builder.validate() + const result = builder.validate() // Return validation result for UI return NextResponse.json({ @@ -559,8 +559,8 @@ export async function logExecutionContext(context: ExtendedWorkflowContext) { workflow: sanitized.workflowId, tenant: sanitized.tenantId, user: sanitized.userId, - mode: (sanitized.multiTenant as Record)?.executionMode, - timestamp: (sanitized.multiTenant as Record)?.requestedAt, + mode: (sanitized.multiTenant as Record).executionMode, + timestamp: (sanitized.multiTenant as Record).requestedAt, // Variables listed as keys only, no values variables: sanitized.variables, // Limits included for monitoring @@ -657,7 +657,7 @@ export async function retryFailedWorkflowExecution( async function verifyUserAuth(req: NextRequest): Promise { // In production, parse JWT and get user details const authHeader = req.headers.get('authorization') - if (authHeader === null || !authHeader.startsWith('Bearer ')) { + if (authHeader?.startsWith('Bearer ') !== true) { return null } diff --git a/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts b/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts index 6aa1e7d69..5718034ac 100644 --- a/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts +++ b/frontends/nextjs/src/lib/workflow/multi-tenant-context.ts @@ -189,7 +189,7 @@ export class MultiTenantContextBuilder { triggerData: requestData?.triggerData ?? {}, variables: this.buildVariables(requestData?.variables), secrets: requestData?.secrets ?? {}, - request: this.options.captureRequestData ? requestData?.request as WorkflowContext['request'] : undefined, + request: this.options.captureRequestData === true ? requestData?.request as WorkflowContext['request'] : undefined, multiTenant: multiTenantMeta, requestMetadata: { ipAddress: this.requestContext.ipAddress, @@ -205,15 +205,15 @@ export class MultiTenantContextBuilder { this.validateContextSafety(context) // 5. Load and bind credentials - if (this.options.enforceCredentialValidation) { - await this.bindCredentials(context) + if (this.options.enforceCredentialValidation === true) { + this.bindCredentials(context) } // 6. Validate variables don't cross tenants this.validateVariableTenantIsolation(context) // 7. Log context creation (audit) - if (this.options.enableAuditLogging) { + if (this.options.enableAuditLogging === true) { this.logContextCreation(context) } @@ -231,13 +231,12 @@ export class MultiTenantContextBuilder { // Super-admin (level 4) can access any tenant if (this.requestContext.userLevel >= 4) { - if (!this.options.allowCrossTenantAccess) { + if (this.options.allowCrossTenantAccess !== true) { throw new Error( `Cross-tenant access disabled: User ${this.requestContext.userId} ` + `cannot access workflow in tenant ${this.workflow.tenantId}` ) } - // eslint-disable-next-line no-console console.warn( `[SECURITY] Super-admin ${this.requestContext.userId} accessing ` + `cross-tenant workflow ${this.workflow.id}` @@ -327,28 +326,24 @@ export class MultiTenantContextBuilder { const variables: DataRecord = {} // 1. Add workflow defaults - if (this.workflow.variables != null) { - for (const [varName, varDef] of Object.entries(this.workflow.variables)) { - // Only allow workflow and execution scopes (not global) - if (varDef.scope === 'global') { - // eslint-disable-next-line no-console - console.warn(`[SECURITY] Skipping global-scope variable ${varName} - not allowed`) - continue - } - - variables[varName] = varDef.defaultValue ?? null + for (const [varName, varDef] of Object.entries(this.workflow.variables)) { + // Only allow workflow and execution scopes (not global) + if (varDef.scope === 'global') { + console.warn(`[SECURITY] Skipping global-scope variable ${varName} - not allowed`) + continue } + + variables[varName] = varDef.defaultValue ?? null } // 2. Merge request overrides if (requestVariables != null) { for (const [varName, varValue] of Object.entries(requestVariables)) { // Validate variable is allowed by workflow - const varDef = this.workflow.variables?.[varName] + const varDef = this.workflow.variables[varName] if (varDef != null) { variables[varName] = varValue } else { - // eslint-disable-next-line no-console console.warn( `[SECURITY] Rejecting unknown variable ${varName} - not in workflow definition` ) @@ -371,7 +366,7 @@ export class MultiTenantContextBuilder { const errors: string[] = [] // 1. Tenant ID must match (unless cross-tenant access is explicitly allowed) - if (context.tenantId !== this.workflow.tenantId && !this.options.allowCrossTenantAccess) { + if (context.tenantId !== this.workflow.tenantId && this.options.allowCrossTenantAccess !== true) { errors.push( `Context tenant ${context.tenantId} does not match ` + `workflow tenant ${this.workflow.tenantId}` @@ -389,13 +384,12 @@ export class MultiTenantContextBuilder { } // 4. Check execution limits - if (this.workflow.executionLimits != null) { - if (context.executionLimits.maxExecutionTime > this.workflow.executionLimits.maxExecutionTime) { - errors.push( - `Requested execution time (${String(context.executionLimits.maxExecutionTime)}ms) ` + - `exceeds workflow limit (${String(this.workflow.executionLimits.maxExecutionTime)}ms)` - ) - } + const workflowLimit = this.workflow.executionLimits ?? this.getDefaultExecutionLimits() + if (context.executionLimits.maxExecutionTime > workflowLimit.maxExecutionTime) { + errors.push( + `Requested execution time (${String(context.executionLimits.maxExecutionTime)}ms) ` + + `exceeds workflow limit (${String(workflowLimit.maxExecutionTime)}ms)` + ) } if (errors.length > 0) { @@ -425,8 +419,8 @@ export class MultiTenantContextBuilder { /** * Load and bind credentials from workflow definition */ - private async bindCredentials(context: ExtendedWorkflowContext): Promise { - const bindings = this.workflow.credentials ?? [] + private bindCredentials(context: ExtendedWorkflowContext): void { + const bindings = this.workflow.credentials for (const binding of bindings) { try { @@ -449,7 +443,6 @@ export class MultiTenantContextBuilder { name: binding.credentialName, }) } catch (error: unknown) { - // eslint-disable-next-line no-console console.error(`Failed to bind credential for node ${binding.nodeId}:`, error) throw error } @@ -535,7 +528,7 @@ export class MultiTenantContextBuilder { } // 4. Check for global scope variables - for (const [varName, varDef] of Object.entries(this.workflow.variables ?? {})) { + for (const [varName, varDef] of Object.entries(this.workflow.variables)) { if (varDef.scope === 'global') { warnings.push({ path: `variables.${varName}`, @@ -546,8 +539,8 @@ export class MultiTenantContextBuilder { } // 5. Check credentials - const credentialCount = this.workflow.credentials?.length ?? 0 - if (this.options.enforceCredentialValidation && credentialCount > 0) { + const credentialCount = this.workflow.credentials.length + if (this.options.enforceCredentialValidation === true && credentialCount > 0) { warnings.push({ path: 'credentials', message: `${String(credentialCount)} credential(s) will be validated during execution`, diff --git a/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts b/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts index c65f15805..b4e8520ca 100644 --- a/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts +++ b/frontends/nextjs/src/lib/workflow/workflow-error-handler.ts @@ -387,9 +387,9 @@ export class WorkflowErrorHandler { ): WorkflowApiResponse { const code = this.getErrorCode(error) const message = this.isDevelopment - ? (this.getErrorMessage(error) ?? ERROR_MESSAGES[code]) - : (ERROR_MESSAGES[code] ?? this.getErrorMessage(error)) - const statusCode = ERROR_STATUS_MAP[code] ?? 500 + ? this.getErrorMessage(error) + : ERROR_MESSAGES[code] + const statusCode = ERROR_STATUS_MAP[code] const response: FormattedError = { success: false, @@ -462,7 +462,7 @@ export class WorkflowErrorHandler { errorCode: WorkflowErrorCode, context: ErrorContext = {} ): WorkflowApiResponse { - const statusCode = ERROR_STATUS_MAP[errorCode] ?? 401 + const statusCode = ERROR_STATUS_MAP[errorCode] const response: FormattedError = { success: false, @@ -636,7 +636,7 @@ export class WorkflowErrorHandler { * Get suggestion for validation error */ private getSuggestionForError(error: ValidationError): string { - const code = (error.code ?? '').toUpperCase() + const code = error.code.toUpperCase() const suggestions: Record = { MISSING_REQUIRED_FIELD: 'Add the missing parameter to the node.', INVALID_NODE_TYPE: 'Use a valid node type from the registry.', diff --git a/frontends/nextjs/src/lib/workflow/workflow-loader-v2.ts b/frontends/nextjs/src/lib/workflow/workflow-loader-v2.ts index 50351c478..003092abf 100644 --- a/frontends/nextjs/src/lib/workflow/workflow-loader-v2.ts +++ b/frontends/nextjs/src/lib/workflow/workflow-loader-v2.ts @@ -282,7 +282,6 @@ export class ValidationCache { } if (cleaned > 0) { - // eslint-disable-next-line no-console console.warn(`[CACHE CLEANUP] Removed ${cleaned} expired entries`) } }, 5 * 60 * 1000) // Every 5 minutes @@ -416,7 +415,6 @@ export class WorkflowLoaderV2 { const cached = this.cache.get(cacheKey) if (cached != null) { if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn(`[CACHE HIT] Validation for workflow ${workflow.id}`) } return { @@ -430,7 +428,6 @@ export class WorkflowLoaderV2 { const existingValidation = this.activeValidations.get(validationKey) if (existingValidation != null) { if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn( `[DEDUP] Reusing in-flight validation for ${validationKey}` ) @@ -475,7 +472,6 @@ export class WorkflowLoaderV2 { workflows: WorkflowDefinition[] ): Promise { if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn(`Starting batch validation for ${workflows.length} workflows`) } @@ -541,7 +537,6 @@ export class WorkflowLoaderV2 { const cacheKey = `${tenantId}:${workflowId}` this.cache.delete(cacheKey) if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn(`[CACHE INVALIDATED] ${workflowId}`) } } @@ -596,7 +591,6 @@ export class WorkflowLoaderV2 { clearCache(): void { this.cache.clear() if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn('All validation caches cleared') } } @@ -662,7 +656,6 @@ export class WorkflowLoaderV2 { const duration = Date.now() - startTime if (this.enableLogging) { - // eslint-disable-next-line no-console console.warn(`[VALIDATION] Workflow ${workflow.id} validated in ${duration}ms`, { nodeCount: workflow.nodes.length, connectionCount: Object.keys(workflow.connections).length, @@ -869,9 +862,7 @@ let globalLoader: WorkflowLoaderV2 | null = null export function getWorkflowLoader( options?: WorkflowLoaderV2Options ): WorkflowLoaderV2 { - if (globalLoader == null) { - globalLoader = new WorkflowLoaderV2(options) - } + globalLoader ??= new WorkflowLoaderV2(options) return globalLoader } diff --git a/frontends/nextjs/src/store/store.ts b/frontends/nextjs/src/store/store.ts index b139b1ea3..d6c8c71d8 100644 --- a/frontends/nextjs/src/store/store.ts +++ b/frontends/nextjs/src/store/store.ts @@ -46,14 +46,14 @@ const { store, persistor } = createPersistedStore({ middleware: (base: Middleware[]) => { const middlewares: Middleware[] = [...base] if (isDev) { - middlewares.push(createLoggingMiddleware({ verbose: false }) as Middleware) - middlewares.push(createPerformanceMiddleware() as Middleware) + middlewares.push(createLoggingMiddleware({ verbose: false })) + middlewares.push(createPerformanceMiddleware()) } - middlewares.push(createAnalyticsMiddleware() as Middleware) - middlewares.push(createErrorMiddleware() as Middleware) + middlewares.push(createAnalyticsMiddleware()) + middlewares.push(createErrorMiddleware()) return middlewares }, - devTools: getDevToolsConfig(), + devTools: getDevToolsConfig() as boolean | object, ignoredActions: ['asyncData/fetchAsyncData/pending'], ignoredPaths: ['asyncData.requests.*.promise'], })