Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/sim/app/api/auth/forget-password/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json(
{
message:
// utils-lint-allow: returned to an unauthenticated caller, so a non-Error throw
// must surface the fixed copy rather than its own text — getErrorMessage would
// pass a thrown string straight through.
error instanceof Error
? error.message
: 'Failed to send password reset email. Please try again later.',
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json(
{
message:
// utils-lint-allow: returned to an unauthenticated caller, so a non-Error throw
// must surface the fixed copy rather than its own text — getErrorMessage would
// pass a thrown string straight through.
error instanceof Error
? error.message
: 'Failed to reset password. Please try again or request a new reset link.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,7 @@ export function TeamManagement({
portalWindow?.close()
logger.error('Failed to open billing portal from transfer dialog', { error })
setTransferPortalError(
error instanceof Error
? error.message
: 'Failed to open Stripe billing portal. Please try again.'
getErrorMessage(error, 'Failed to open Stripe billing portal. Please try again.')
)
},
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import type { Edge } from 'reactflow'
import { ApiClientError } from '@/lib/api/client/errors'
import { requestJson } from '@/lib/api/client/request'
import {
putWorkflowNormalizedStateContract,
Expand Down Expand Up @@ -100,12 +99,7 @@ export async function applyAutoLayoutAndUpdateStore(
},
})
} catch (error) {
const errorMessage =
error instanceof ApiClientError
? error.message
: error instanceof Error
? error.message
: 'Auto layout failed'
const errorMessage = getErrorMessage(error, 'Auto layout failed')
logger.error('Auto layout API call failed:', { error: errorMessage })
return { success: false, error: errorMessage }
}
Expand Down
4 changes: 1 addition & 3 deletions apps/sim/lib/billing/client/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,7 @@ export function useSubscriptionUpgrade() {
error:
transferError instanceof ApiClientError
? (transferError.rawBody ?? transferError.message)
: transferError instanceof Error
? transferError.message
: 'Unknown error',
: getErrorMessage(transferError, 'Unknown error'),
})
}
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/execution/remote-sandbox/e2b.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ function isE2BExecutionTimeout(error: unknown): boolean {
? error.name
: ''
const message =
// utils-lint-allow: probes E2B's own error shape — a record-like carrying `message`
// or `value` — which getErrorMessage cannot express.
error instanceof Error
? error.message
: isRecordLike(error)
Expand Down
6 changes: 2 additions & 4 deletions apps/sim/lib/webhooks/providers/microsoft-teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Base64 } from '@sim/security/hmac'
import { toError } from '@sim/utils/errors'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
Expand Down Expand Up @@ -733,9 +733,7 @@ export const microsoftTeamsHandler: WebhookProviderHandler = {
error
)
throw new Error(
error instanceof Error
? error.message
: 'Failed to create Teams subscription. Please try again.'
getErrorMessage(error, 'Failed to create Teams subscription. Please try again.')
)
}
},
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/lib/webhooks/providers/telegram.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { db, webhook, workflowDeploymentVersion } from '@sim/db'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq, isNull, ne } from 'drizzle-orm'
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type {
Expand Down Expand Up @@ -170,9 +171,7 @@ export const telegramHandler: WebhookProviderHandler = {
error
)
throw new Error(
error instanceof Error
? error.message
: 'Failed to create Telegram webhook. Please try again.'
getErrorMessage(error, 'Failed to create Telegram webhook. Please try again.')
)
}
},
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/lib/webhooks/providers/typeform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Base64 } from '@sim/security/hmac'
import { getErrorMessage } from '@sim/utils/errors'
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type {
DeleteSubscriptionContext,
Expand Down Expand Up @@ -168,9 +169,7 @@ export const typeformHandler: WebhookProviderHandler = {
error
)
throw new Error(
error instanceof Error
? error.message
: 'Failed to create Typeform webhook. Please try again.'
getErrorMessage(error, 'Failed to create Typeform webhook. Please try again.')
)
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { getPostgresErrorCode, toError } from '@sim/utils/errors'
import { getErrorMessage, getPostgresErrorCode, toError } from '@sim/utils/errors'
import { asOrchestrationError, type OrchestrationErrorCode } from '@/lib/core/orchestration/types'
import { FolderPathError } from '@/lib/folders/paths'
import { notifyWorkspaceFilesChanged } from '@/lib/realtime/notify'
Expand Down Expand Up @@ -385,10 +385,10 @@ export async function performMoveWorkspaceFileItems(
) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'A file or folder with this name already exists in the destination folder',
error: getErrorMessage(
error,
'A file or folder with this name already exists in the destination folder'
),
errorCode: 'conflict',
}
}
Expand Down
9 changes: 5 additions & 4 deletions apps/sim/stores/workflows/registry/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateRandomHex } from '@sim/utils/random'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
Expand Down Expand Up @@ -206,10 +207,10 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(

logger.info(`Switched to workflow ${workflowId}`)
} catch (error) {
const message =
error instanceof Error
? error.message
: `Failed to load workflow ${workflowId}: Unknown error`
const message = getErrorMessage(
error,
`Failed to load workflow ${workflowId}: Unknown error`
)
logger.error(message)

const currentHydration = get().hydration
Expand Down
82 changes: 66 additions & 16 deletions scripts/check-utils-enforcement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
/**
* Enforces use of shared @sim/utils helpers over inline implementations.
*
* Biome's noRestrictedImports covers import-based bans (nanoid, uuid, crypto named imports).
* This script catches patterns that static import analysis misses — global property access,
* inline idioms, and reimplemented helpers that should live in @sim/utils.
* Biome's noRestrictedImports covers the import-based bans it lists — today `nanoid` and
* `uuid`. It does NOT cover named crypto imports; `import { randomBytes } from 'node:crypto'`
* passes both gates, and deliberately so, since server code building cipher IVs and tokens
* wants node's crypto rather than the cross-context wrapper in `@sim/utils/random`.
*
* This script catches what static import analysis misses — global property access, inline
* idioms, and reimplemented helpers that should live in @sim/utils.
*
* Patterns are matched against the whole file, not line by line: every idiom banned here is a
* multi-token expression that the formatter wraps at 100 columns, and a line-scoped scan sees
* none of the wrapped forms. Deliberate exceptions carry `// utils-lint-allow: <reason>`.
*/
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
Expand Down Expand Up @@ -108,6 +116,48 @@ interface Violation {
snippet: string
}

/** Escape hatch for a deliberate use, mirroring `rq-lint-allow:` in check-react-query-patterns.ts. */
const ALLOW = 'utils-lint-allow:'

/** Offset of the first character of each line, for mapping a match index back to a line number. */
function buildLineStarts(content: string): number[] {
const starts = [0]
for (let i = 0; i < content.length; i++) {
if (content[i] === '\n') starts.push(i + 1)
}
return starts
}

/** 1-based line containing `offset`, by binary search over {@link buildLineStarts}. */
function lineAt(lineStarts: number[], offset: number): number {
let low = 0
let high = lineStarts.length - 1
while (low < high) {
const mid = Math.ceil((low + high) / 2)
if (lineStarts[mid] <= offset) low = mid
else high = mid - 1
}
return low + 1
}

/**
* True if a `// utils-lint-allow: <reason>` annotation sits just above `line` (1-based).
*
* The reason must be non-empty: an annotation that does not say why is the thing this
* check exists to prevent. Scans up to three comment lines above, so the annotation can
* carry context lines with it.
*/
function hasAllow(lines: string[], line: number): boolean {
for (let i = line - 2; i >= 0 && i >= line - 5; i--) {
const text = lines[i]?.trim() ?? ''
if (text.includes(ALLOW)) {
return text.slice(text.indexOf(ALLOW) + ALLOW.length).trim().length > 0
}
if (text.length > 0 && !text.startsWith('//') && !text.startsWith('*')) break
}
return false
}

async function main() {
const allFiles: string[] = []
for (const dir of SCAN_DIRS) {
Expand All @@ -122,20 +172,20 @@ async function main() {

const content = await readFile(file, 'utf8')
const lines = content.split('\n')
const lineStarts = buildLineStarts(content)

for (let i = 0; i < lines.length; i++) {
const line = lines[i]
for (const { pattern, description, suggestion } of BANNED_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(line)) {
violations.push({
file: rel,
line: i + 1,
description,
suggestion,
snippet: line.trim(),
})
}
for (const { pattern, description, suggestion } of BANNED_PATTERNS) {
pattern.lastIndex = 0
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
const line = lineAt(lineStarts, match.index)
if (hasAllow(lines, line)) continue
violations.push({
file: rel,
line,
description,
suggestion,
snippet: (lines[line - 1] ?? '').trim(),
})
}
}
}
Expand Down
Loading