Skip to content
  •  
  •  
  •  
20 changes: 20 additions & 0 deletions .claude/rules/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,25 @@ const shortId = generateShortId()
const tiny = generateShortId(8)
```

## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:

- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`

```typescript
// ✗ Bad
await new Promise(resolve => setTimeout(resolve, 1000))
const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))

// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)
```

## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.
20 changes: 20 additions & 0 deletions .cursor/rules/global.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,25 @@ const shortId = generateShortId()
const tiny = generateShortId(8)
```

## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:

- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`

```typescript
// ✗ Bad
await new Promise(resolve => setTimeout(resolve, 1000))
const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))

// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)
```

## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ You are a professional software engineer. All code must follow best practices: a
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values.
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`

## Architecture
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/academy/components/sandbox-canvas-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from '@/lib/academy/types'
import { validateExercise } from '@/lib/academy/validation'
import { cn } from '@/lib/core/utils/cn'
import { sleep } from '@/lib/core/utils/helpers'
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
Expand Down Expand Up @@ -323,7 +324,7 @@ export function SandboxCanvasProvider({
for (let i = 0; i < plan.length; i++) {
const step = plan[i]
setActiveBlocks(workflowId, new Set([step.blockId]))
await new Promise((resolve) => setTimeout(resolve, step.delay))
await sleep(step.delay)
addConsole({
workflowId,
blockId: step.blockId,
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { account, credential, credentialSetMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq, inArray } from 'drizzle-orm'
import { decryptSecret } from '@/lib/core/security/encryption'
import { toError } from '@/lib/core/utils/helpers'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,
Expand Down Expand Up @@ -331,7 +332,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
return accessToken
} catch (error) {
logger.error(`Error refreshing token for user ${userId}, provider ${providerId}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
providerId,
userId,
Expand Down Expand Up @@ -460,7 +461,7 @@ export async function refreshAccessTokenIfNeeded(
return refreshedToken.accessToken
} catch (error) {
logger.error(`[${requestId}] Error refreshing token for credential`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
providerId: credential.providerId,
credentialId,
Expand Down Expand Up @@ -664,7 +665,7 @@ export async function getCredentialsForCredentialSet(
}
} catch (error) {
logger.error(`Failed to refresh token for user ${cred.userId}, provider ${providerId}`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
continue
}
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/auth/oauth2/shopify/store/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'

Expand Down Expand Up @@ -113,7 +114,7 @@ export async function GET(request: NextRequest) {

const returnUrl = request.cookies.get('shopify_return_url')?.value

const redirectUrl = returnUrl || `${baseUrl}/workspace`
const redirectUrl = returnUrl && isSameOrigin(returnUrl) ? returnUrl : `${baseUrl}/workspace`
const finalUrl = new URL(redirectUrl)
finalUrl.searchParams.set('shopify_connected', 'true')

Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/auth/shopify/authorize/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { getScopesForService } from '@/lib/oauth/utils'

const logger = createLogger('ShopifyAuthorize')
Expand Down Expand Up @@ -192,7 +193,7 @@ export async function GET(request: NextRequest) {
path: '/',
})

if (returnUrl) {
if (returnUrl && isSameOrigin(returnUrl)) {
response.cookies.set('shopify_return_url', returnUrl, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/auth/socket-token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'

const logger = createLogger('SocketTokenAPI')

Expand Down Expand Up @@ -36,7 +37,7 @@ export async function POST() {
}

logger.error('Failed to generate socket token', {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
Expand Down
26 changes: 26 additions & 0 deletions apps/sim/app/api/auth/sso/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@ export async function POST(request: NextRequest) {
oidcConfig.userInfoEndpoint = userInfoEndpoint
oidcConfig.jwksEndpoint = jwksEndpoint

const userProvidedEndpoints: Record<string, string | undefined> = {
authorizationEndpoint,
tokenEndpoint,
userInfoEndpoint,
jwksEndpoint,
}

for (const [name, endpointUrl] of Object.entries(userProvidedEndpoints)) {
if (endpointUrl) {
const endpointValidation = await validateUrlWithDNS(endpointUrl, `OIDC ${name}`)
if (!endpointValidation.isValid) {
logger.warn('Explicitly provided OIDC endpoint failed SSRF validation', {
endpoint: name,
url: endpointUrl,
error: endpointValidation.error,
})
return NextResponse.json(
{
error: `OIDC ${name} failed security validation: ${endpointValidation.error}`,
},
{ status: 400 }
)
}
}
}

const needsDiscovery =
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint

Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/billing/switch-plan/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
hasUsableSubscriptionStatus,
} from '@/lib/billing/subscriptions/utils'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'
import { captureServerEvent } from '@/lib/posthog/server'

const logger = createLogger('SwitchPlan')
Expand Down Expand Up @@ -185,7 +186,7 @@ export async function POST(request: NextRequest) {
} catch (error) {
logger.error('Failed to switch subscription', {
userId: session?.user?.id,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to switch plan' },
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/billing/update-cost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/request/http'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'

const logger = createLogger('BillingUpdateCostAPI')
Expand Down Expand Up @@ -170,7 +171,7 @@ export async function POST(req: NextRequest) {
const duration = Date.now() - startTime

logger.error(`[${requestId}] Cost update failed`, {
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
duration,
})
Expand All @@ -180,7 +181,7 @@ export async function POST(req: NextRequest) {
.release(claim.normalizedKey, claim.storageMethod)
.catch((releaseErr) => {
logger.warn(`[${requestId}] Failed to release idempotency claim`, {
error: releaseErr instanceof Error ? releaseErr.message : String(releaseErr),
error: toError(releaseErr).message,
normalizedKey: claim?.normalizedKey,
})
})
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/copilot/chat/abort/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session'
import { env } from '@/lib/core/config/env'
import { toError } from '@/lib/core/utils/helpers'

const logger = createLogger('CopilotChatAbortAPI')
const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000
Expand All @@ -20,7 +21,7 @@ export async function POST(request: Request) {

const body = await request.json().catch((err) => {
logger.warn('Abort request body parse failed; continuing with empty object', {
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return {}
})
Expand All @@ -35,7 +36,7 @@ export async function POST(request: Request) {
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
logger.warn('getLatestRunForStream failed while resolving chatId for abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
Expand Down Expand Up @@ -70,7 +71,7 @@ export async function POST(request: Request) {
} catch (err) {
logger.warn('Explicit abort marker request failed; proceeding with local abort', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
}

Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/copilot/chat/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
import { readEvents } from '@/lib/copilot/request/session/buffer'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { toError } from '@/lib/core/utils/helpers'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

Expand Down Expand Up @@ -82,15 +83,15 @@ export async function GET(req: NextRequest) {
logger.warn('Failed to read preview sessions for copilot chat', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return []
}),
getLatestRunForStream(chat.conversationId, authenticatedUserId).catch((error) => {
logger.warn('Failed to fetch latest run for copilot chat snapshot', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return null
}),
Expand All @@ -110,7 +111,7 @@ export async function GET(req: NextRequest) {
logger.warn('Failed to load copilot chat stream snapshot', {
chatId,
conversationId: chat.conversationId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
}
}
Expand Down
11 changes: 6 additions & 5 deletions apps/sim/app/api/copilot/chat/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SSE_RESPONSE_HEADERS,
} from '@/lib/copilot/request/session'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { sleep, toError } from '@/lib/core/utils/helpers'

export const maxDuration = 3600

Expand Down Expand Up @@ -97,7 +98,7 @@ export async function GET(request: NextRequest) {
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
logger.warn('Failed to fetch latest run for stream', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
Expand All @@ -119,7 +120,7 @@ export async function GET(request: NextRequest) {
readFilePreviewSessions(streamId).catch((error) => {
logger.warn('Failed to read preview sessions for stream batch', {
streamId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return []
}),
Expand Down Expand Up @@ -235,7 +236,7 @@ export async function GET(request: NextRequest) {
(err) => {
logger.warn('Failed to poll latest run for stream', {
streamId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
}
Expand Down Expand Up @@ -273,7 +274,7 @@ export async function GET(request: NextRequest) {
break
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
await sleep(POLL_INTERVAL_MS)
}
if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) {
emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, {
Expand All @@ -286,7 +287,7 @@ export async function GET(request: NextRequest) {
if (!controllerClosed && !request.signal.aborted) {
logger.warn('Stream replay failed', {
streamId,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, {
message: 'The stream replay failed before completion.',
Expand Down
7 changes: 4 additions & 3 deletions apps/sim/app/api/copilot/confirm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'

const logger = createLogger('CopilotConfirmAPI')

Expand Down Expand Up @@ -106,7 +107,7 @@ async function updateToolCallStatus(
logger.error('Failed to update tool call status', {
toolCallId,
status,
error: error instanceof Error ? error.message : String(error),
error: toError(error).message,
})
return false
}
Expand All @@ -133,7 +134,7 @@ export async function POST(req: NextRequest) {
const existing = await getAsyncToolCall(toolCallId).catch((err) => {
logger.warn('Failed to fetch async tool call', {
toolCallId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
Expand All @@ -145,7 +146,7 @@ export async function POST(req: NextRequest) {
const run = await getRunSegment(existing.runId).catch((err) => {
logger.warn('Failed to fetch run segment', {
runId: existing.runId,
error: err instanceof Error ? err.message : String(err),
error: toError(err).message,
})
return null
})
Expand Down
Loading
Loading