Skip to content

Commit a05b442

Browse files
waleedlatif1claude
andcommitted
improvement(utils): add shared utility functions and replace inline patterns
Add sleep, toError, safeJsonParse, isNonNull helpers and invariant/assertNever assertions. Replace all inline implementations across the codebase with these shared utilities for consistency. Zero behavioral changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38864fa commit a05b442

File tree

329 files changed

+1389
-790
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

329 files changed

+1389
-790
lines changed

.claude/rules/global.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,32 @@ const shortId = generateShortId()
3030
const tiny = generateShortId(8)
3131
```
3232

33+
## Common Utilities
34+
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
35+
36+
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
37+
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
38+
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
39+
- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }`
40+
- `isNonNull(value)` — type-narrowing filter predicate for null/undefined
41+
42+
Use assertion utilities from `@/lib/core/utils/asserts`:
43+
44+
- `invariant(condition, message)` — assert a condition is truthy, throws if not
45+
- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled
46+
47+
```typescript
48+
// ✗ Bad
49+
await new Promise(resolve => setTimeout(resolve, 1000))
50+
const msg = error instanceof Error ? error.message : String(error)
51+
const err = error instanceof Error ? error : new Error(String(error))
52+
53+
// ✓ Good
54+
import { sleep, toError } from '@/lib/core/utils/helpers'
55+
await sleep(1000)
56+
const msg = toError(error).message
57+
const err = toError(error)
58+
```
59+
3360
## Package Manager
3461
Use `bun` and `bunx`, not `npm` and `npx`.

.cursor/rules/global.mdc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,32 @@ const shortId = generateShortId()
3737
const tiny = generateShortId(8)
3838
```
3939

40+
## Common Utilities
41+
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
42+
43+
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
44+
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
45+
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
46+
- `safeJsonParse(str, fallback?)` — parse JSON without throwing. Never write `try { JSON.parse(str) } catch { return default }`
47+
- `isNonNull(value)` — type-narrowing filter predicate for null/undefined
48+
49+
Use assertion utilities from `@/lib/core/utils/asserts`:
50+
51+
- `invariant(condition, message)` — assert a condition is truthy, throws if not
52+
- `assertNever(value)` — exhaustive switch/if-else check, TypeScript errors at compile time if a case is unhandled
53+
54+
```typescript
55+
// ✗ Bad
56+
await new Promise(resolve => setTimeout(resolve, 1000))
57+
const msg = error instanceof Error ? error.message : String(error)
58+
const err = error instanceof Error ? error : new Error(String(error))
59+
60+
// ✓ Good
61+
import { sleep, toError } from '@/lib/core/utils/helpers'
62+
await sleep(1000)
63+
const msg = toError(error).message
64+
const err = toError(error)
65+
```
66+
4067
## Package Manager
4168
Use `bun` and `bunx`, not `npm` and `npx`.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ You are a professional software engineer. All code must follow best practices: a
88
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
99
- **Styling**: Never update global styles. Keep all styling local to components
1010
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
11+
- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values, `safeJsonParse(str, fallback?)` for safe JSON parsing, `isNonNull(v)` for type-narrowing null filters. Use `invariant(cond, msg)` and `assertNever(val)` from `@/lib/core/utils/asserts` for runtime assertions and exhaustive checks.
1112
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
1213

1314
## Architecture

apps/sim/app/academy/components/sandbox-canvas-provider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from '@/lib/academy/types'
1313
import { validateExercise } from '@/lib/academy/validation'
1414
import { cn } from '@/lib/core/utils/cn'
15+
import { sleep } from '@/lib/core/utils/helpers'
1516
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
1617
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
1718
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
@@ -323,7 +324,7 @@ export function SandboxCanvasProvider({
323324
for (let i = 0; i < plan.length; i++) {
324325
const step = plan[i]
325326
setActiveBlocks(workflowId, new Set([step.blockId]))
326-
await new Promise((resolve) => setTimeout(resolve, step.delay))
327+
await sleep(step.delay)
327328
addConsole({
328329
workflowId,
329330
blockId: step.blockId,

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { account, credential, credentialSetMember } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
55
import { and, desc, eq, inArray } from 'drizzle-orm'
66
import { decryptSecret } from '@/lib/core/security/encryption'
7+
import { toError } from '@/lib/core/utils/helpers'
78
import { refreshOAuthToken } from '@/lib/oauth'
89
import {
910
getMicrosoftRefreshTokenExpiry,
@@ -331,7 +332,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
331332
return accessToken
332333
} catch (error) {
333334
logger.error(`Error refreshing token for user ${userId}, provider ${providerId}`, {
334-
error: error instanceof Error ? error.message : String(error),
335+
error: toError(error).message,
335336
stack: error instanceof Error ? error.stack : undefined,
336337
providerId,
337338
userId,
@@ -460,7 +461,7 @@ export async function refreshAccessTokenIfNeeded(
460461
return refreshedToken.accessToken
461462
} catch (error) {
462463
logger.error(`[${requestId}] Error refreshing token for credential`, {
463-
error: error instanceof Error ? error.message : String(error),
464+
error: toError(error).message,
464465
stack: error instanceof Error ? error.stack : undefined,
465466
providerId: credential.providerId,
466467
credentialId,
@@ -664,7 +665,7 @@ export async function getCredentialsForCredentialSet(
664665
}
665666
} catch (error) {
666667
logger.error(`Failed to refresh token for user ${cred.userId}, provider ${providerId}`, {
667-
error: error instanceof Error ? error.message : String(error),
668+
error: toError(error).message,
668669
})
669670
continue
670671
}

apps/sim/app/api/auth/socket-token/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { headers } from 'next/headers'
33
import { NextResponse } from 'next/server'
44
import { auth } from '@/lib/auth'
55
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
6+
import { toError } from '@/lib/core/utils/helpers'
67

78
const logger = createLogger('SocketTokenAPI')
89

@@ -36,7 +37,7 @@ export async function POST() {
3637
}
3738

3839
logger.error('Failed to generate socket token', {
39-
error: error instanceof Error ? error.message : String(error),
40+
error: toError(error).message,
4041
stack: error instanceof Error ? error.stack : undefined,
4142
})
4243
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })

apps/sim/app/api/auth/sso/register/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,32 @@ export async function POST(request: NextRequest) {
147147
oidcConfig.userInfoEndpoint = userInfoEndpoint
148148
oidcConfig.jwksEndpoint = jwksEndpoint
149149

150+
const userProvidedEndpoints: Record<string, string | undefined> = {
151+
authorizationEndpoint,
152+
tokenEndpoint,
153+
userInfoEndpoint,
154+
jwksEndpoint,
155+
}
156+
157+
for (const [name, endpointUrl] of Object.entries(userProvidedEndpoints)) {
158+
if (endpointUrl) {
159+
const endpointValidation = await validateUrlWithDNS(endpointUrl, `OIDC ${name}`)
160+
if (!endpointValidation.isValid) {
161+
logger.warn('Explicitly provided OIDC endpoint failed SSRF validation', {
162+
endpoint: name,
163+
url: endpointUrl,
164+
error: endpointValidation.error,
165+
})
166+
return NextResponse.json(
167+
{
168+
error: `OIDC ${name} failed security validation: ${endpointValidation.error}`,
169+
},
170+
{ status: 400 }
171+
)
172+
}
173+
}
174+
}
175+
150176
const needsDiscovery =
151177
!oidcConfig.authorizationEndpoint || !oidcConfig.tokenEndpoint || !oidcConfig.jwksEndpoint
152178

apps/sim/app/api/billing/switch-plan/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
hasUsableSubscriptionStatus,
1818
} from '@/lib/billing/subscriptions/utils'
1919
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
20+
import { toError } from '@/lib/core/utils/helpers'
2021
import { captureServerEvent } from '@/lib/posthog/server'
2122

2223
const logger = createLogger('SwitchPlan')
@@ -185,7 +186,7 @@ export async function POST(request: NextRequest) {
185186
} catch (error) {
186187
logger.error('Failed to switch subscription', {
187188
userId: session?.user?.id,
188-
error: error instanceof Error ? error.message : String(error),
189+
error: toError(error).message,
189190
})
190191
return NextResponse.json(
191192
{ error: error instanceof Error ? error.message : 'Failed to switch plan' },

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
77
import { checkInternalApiKey } from '@/lib/copilot/request/http'
88
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
99
import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service'
10+
import { toError } from '@/lib/core/utils/helpers'
1011
import { generateRequestId } from '@/lib/core/utils/request'
1112

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

172173
logger.error(`[${requestId}] Cost update failed`, {
173-
error: error instanceof Error ? error.message : String(error),
174+
error: toError(error).message,
174175
stack: error instanceof Error ? error.stack : undefined,
175176
duration,
176177
})
@@ -180,7 +181,7 @@ export async function POST(req: NextRequest) {
180181
.release(claim.normalizedKey, claim.storageMethod)
181182
.catch((releaseErr) => {
182183
logger.warn(`[${requestId}] Failed to release idempotency claim`, {
183-
error: releaseErr instanceof Error ? releaseErr.message : String(releaseErr),
184+
error: toError(releaseErr).message,
184185
normalizedKey: claim?.normalizedKey,
185186
})
186187
})

apps/sim/app/api/copilot/chat/abort/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
55
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
66
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session'
77
import { env } from '@/lib/core/config/env'
8+
import { toError } from '@/lib/core/utils/helpers'
89

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

2122
const body = await request.json().catch((err) => {
2223
logger.warn('Abort request body parse failed; continuing with empty object', {
23-
error: err instanceof Error ? err.message : String(err),
24+
error: toError(err).message,
2425
})
2526
return {}
2627
})
@@ -35,7 +36,7 @@ export async function POST(request: Request) {
3536
const run = await getLatestRunForStream(streamId, authenticatedUserId).catch((err) => {
3637
logger.warn('getLatestRunForStream failed while resolving chatId for abort', {
3738
streamId,
38-
error: err instanceof Error ? err.message : String(err),
39+
error: toError(err).message,
3940
})
4041
return null
4142
})
@@ -70,7 +71,7 @@ export async function POST(request: Request) {
7071
} catch (err) {
7172
logger.warn('Explicit abort marker request failed; proceeding with local abort', {
7273
streamId,
73-
error: err instanceof Error ? err.message : String(err),
74+
error: toError(err).message,
7475
})
7576
}
7677

0 commit comments

Comments
 (0)