Skip to content

Commit e1018f1

Browse files
waleedlatif1claude
andauthored
improvement(utils): add shared utility functions and replace inline patterns (#4214)
* 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> * fix(agiloft): remove import type from .server module to fix client bundle build Turbopack resolves .server.ts modules even for type-only imports, pulling dns/promises into client bundles. Define SecureFetchResponse locally instead. * fix(agiloft): revert to client-safe imports to fix build The SSRF upgrade to input-validation.server introduced dns/promises into client bundles via tools/registry.ts. Revert to the original client-safe validateExternalUrl + fetch. The SSRF DNS-pinning upgrade for agiloft directExecution should be done via API routes in a separate PR. * feat(agiloft): add API route for retrieve_attachment, matching established file patterns Convert retrieve_attachment from directExecution to standard API route pattern, consistent with Slack download and Google Drive download tools. - Create /api/tools/agiloft/retrieve with DNS validation, auth lifecycle, and base64 file response matching the { file: { name, mimeType, data, size } } convention - Update retrieve_attachment tool to use request/transformResponse instead of directExecution, removing the dependency on executeAgiloftRequest from the tool definition - File output type: 'file' enables FileToolProcessor to store downloaded files in execution filesystem automatically * shopify * fix(agiloft): add optional flag to nullable lock record block outputs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(agiloft): revert optional flag on block outputs — property only exists on tool outputs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(utils): remove unused utilities (asserts, safeJsonParse, isNonNull) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 351873a commit e1018f1

File tree

330 files changed

+1447
-809
lines changed

Some content is hidden

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

330 files changed

+1447
-809
lines changed

.claude/rules/global.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,25 @@ 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+
40+
```typescript
41+
// ✗ Bad
42+
await new Promise(resolve => setTimeout(resolve, 1000))
43+
const msg = error instanceof Error ? error.message : String(error)
44+
const err = error instanceof Error ? error : new Error(String(error))
45+
46+
// ✓ Good
47+
import { sleep, toError } from '@/lib/core/utils/helpers'
48+
await sleep(1000)
49+
const msg = toError(error).message
50+
const err = toError(error)
51+
```
52+
3353
## Package Manager
3454
Use `bun` and `bunx`, not `npm` and `npx`.

.cursor/rules/global.mdc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,25 @@ 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+
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+
4060
## Package Manager
4161
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.
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/oauth2/shopify/store/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77
import { getBaseUrl } from '@/lib/core/utils/urls'
8+
import { isSameOrigin } from '@/lib/core/utils/validation'
89
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
910
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
1011

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

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

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

apps/sim/app/api/auth/shopify/authorize/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth'
44
import { env } from '@/lib/core/config/env'
55
import { getBaseUrl } from '@/lib/core/utils/urls'
66
import { generateId } from '@/lib/core/utils/uuid'
7+
import { isSameOrigin } from '@/lib/core/utils/validation'
78
import { getScopesForService } from '@/lib/oauth/utils'
89

910
const logger = createLogger('ShopifyAuthorize')
@@ -192,7 +193,7 @@ export async function GET(request: NextRequest) {
192193
path: '/',
193194
})
194195

195-
if (returnUrl) {
196+
if (returnUrl && isSameOrigin(returnUrl)) {
196197
response.cookies.set('shopify_return_url', returnUrl, {
197198
httpOnly: true,
198199
secure: process.env.NODE_ENV === 'production',

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' },

0 commit comments

Comments
 (0)