Skip to content

Commit e01bcbb

Browse files
committed
improvement(repo): consolidation of boundary helpers + better unknown usage
1 parent 617a9ea commit e01bcbb

554 files changed

Lines changed: 10160 additions & 8455 deletions

File tree

Some content is hidden

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

.agents/skills/add-integration/SKILL.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ export type {Service}UploadResponse = z.output<typeof {service}UploadResponseSch
615615
import { createLogger } from '@sim/logger'
616616
import { NextResponse, type NextRequest } from 'next/server'
617617
import { {service}UploadContract } from '@/lib/api/contracts/{service}-tools'
618-
import { parseRequest, validationErrorResponseFromError } from '@/lib/api/server'
618+
import { parseRequest } from '@/lib/api/server'
619619
import { checkInternalAuth } from '@/lib/auth/hybrid'
620620
import { generateRequestId } from '@/lib/core/utils/request'
621621
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -633,12 +633,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
633633
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
634634
}
635635

636-
let data
637-
try {
638-
;({ body: data } = await parseRequest({service}UploadContract, request))
639-
} catch (error) {
640-
return validationErrorResponseFromError(error)
641-
}
636+
const parsed = await parseRequest({service}UploadContract, request, {})
637+
if (!parsed.success) return parsed.response
638+
const data = parsed.data.body
642639

643640
let fileBuffer: Buffer
644641
let fileName: string

.cursor/rules/sim-architecture.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ Boundary HTTP request and response shapes for all routes under `apps/sim/app/api
6161

6262
- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` and exports both schemas and named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input<typeof createFolderBodySchema>`).
6363
- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts`.
64-
- Routes validate via canonical helpers in `apps/sim/lib/api/server/validation.ts` (`parseRequest`, `validateSchema`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
64+
- Routes validate via canonical helpers in `apps/sim/lib/api/server/validation.ts` (`parseRequest`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
6565
- Clients call `requestJson(contract, ...)` from `apps/sim/lib/api/client/request.ts`; hooks import named type aliases from contracts, never `z.input/z.output`.
6666
- Routes under `apps/sim/app/api/v1/**` use `apps/sim/app/api/v1/middleware.ts` for shared auth, rate-limit, and workspace access. Compose contract validation inside that middleware.
6767
- `bun run check:api-validation` enforces this policy and must pass on PRs.
6868

69-
`bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons. Two per-line opt-out forms are recognized: `// boundary-raw-fetch: <reason>` (placed immediately above a legitimate raw `fetch(` call in `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` for stream/binary/multipart/signed-URL/OAuth-redirect/external-origin cases) and `// double-cast-allowed: <reason>` (placed immediately above an `as unknown as X` cast outside test files). The reason must be non-empty. Whole-file allowlists for routes that legitimately import Zod for non-boundary reasons go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.
69+
`bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons. Four per-line opt-out forms are recognized: `// boundary-raw-fetch: <reason>` (placed immediately above a legitimate raw `fetch(` call in `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` for stream/binary/multipart/signed-URL/OAuth-redirect/external-origin cases), `// double-cast-allowed: <reason>` (placed immediately above an `as unknown as X` cast outside test files), `// boundary-raw-json: <reason>` (placed immediately above a raw `await request.json()` / `await req.json()` read in a route handler that cannot go through `parseRequest` — JSON-RPC envelopes, tolerant `.catch(() => ({}))` parses), and `// untyped-response: <reason>` (placed immediately above a `schema: z.unknown()` response declaration in a contract file when the response body is genuinely opaque). The reason must be non-empty. Whole-file allowlists for routes that legitimately import Zod for non-boundary reasons go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.

AGENTS.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,12 @@ Domain validators that are not HTTP boundaries — tools, blocks, triggers, conn
130130

131131
### Boundary annotations
132132

133-
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes two annotation forms:
133+
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:
134134

135135
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
136136
- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files
137+
- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`
138+
- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)
137139

138140
Placement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`).
139141

@@ -155,8 +157,7 @@ const provider = config as unknown as LegacyProvider
155157

156158
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
157159

158-
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call
159-
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive
160+
- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure
160161
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`
161162
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`
162163
- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`
@@ -168,19 +169,17 @@ import { createLogger } from '@sim/logger'
168169
import type { NextRequest } from 'next/server'
169170
import { NextResponse } from 'next/server'
170171
import { createFolderContract } from '@/lib/api/contracts/folders'
171-
import { parseRequest, validationErrorResponseFromError } from '@/lib/api/server'
172+
import { parseRequest } from '@/lib/api/server'
172173
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
173174

174175
const logger = createLogger('FoldersAPI')
175176

176177
export const POST = withRouteHandler(async (request: NextRequest) => {
177-
try {
178-
const { body } = await parseRequest(createFolderContract, request)
179-
logger.info('Creating folder', { workspaceId: body.workspaceId })
180-
return NextResponse.json({ ok: true })
181-
} catch (error) {
182-
return validationErrorResponseFromError(error)
183-
}
178+
const parsed = await parseRequest(createFolderContract, request, {})
179+
if (!parsed.success) return parsed.response
180+
const { body } = parsed.data
181+
logger.info('Creating folder', { workspaceId: body.workspaceId })
182+
return NextResponse.json({ ok: true })
184183
})
185184
```
186185

CLAUDE.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,12 @@ Domain validators that are not HTTP boundaries — tools, blocks, triggers, conn
109109

110110
### Boundary annotations
111111

112-
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes two annotation forms:
112+
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:
113113

114114
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests
115115
- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files
116+
- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`
117+
- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)
116118

117119
Placement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`).
118120

@@ -136,8 +138,7 @@ Every API route handler must be wrapped with `withRouteHandler`. This sets up `A
136138

137139
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
138140

139-
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call
140-
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive
141+
- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure
141142
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`
142143
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`
143144
- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`
@@ -149,19 +150,17 @@ import { createLogger } from '@sim/logger'
149150
import type { NextRequest } from 'next/server'
150151
import { NextResponse } from 'next/server'
151152
import { createFolderContract } from '@/lib/api/contracts/folders'
152-
import { parseRequest, validationErrorResponseFromError } from '@/lib/api/server'
153+
import { parseRequest } from '@/lib/api/server'
153154
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
154155

155156
const logger = createLogger('FoldersAPI')
156157

157158
export const POST = withRouteHandler(async (request: NextRequest) => {
158-
try {
159-
const { body } = await parseRequest(createFolderContract, request)
160-
logger.info('Creating folder', { workspaceId: body.workspaceId })
161-
return NextResponse.json({ ok: true })
162-
} catch (error) {
163-
return validationErrorResponseFromError(error)
164-
}
159+
const parsed = await parseRequest(createFolderContract, request, {})
160+
if (!parsed.success) return parsed.response
161+
const { body } = parsed.data
162+
logger.info('Creating folder', { workspaceId: body.workspaceId })
163+
return NextResponse.json({ ok: true })
165164
})
166165
```
167166

apps/sim/AGENTS.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ Boundary HTTP request and response shapes for all routes under `apps/sim/app/api
8181

8282
### Boundary annotations
8383

84-
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes two annotation forms:
84+
A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:
8585

8686
- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**`. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests.
8787
- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files.
88+
- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`.
89+
- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough).
8890

8991
Placement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`).
9092

@@ -106,8 +108,7 @@ const provider = config as unknown as LegacyProvider
106108

107109
Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`:
108110

109-
- `parseRequest(contract, request, context)` — fully contract-bound routes; parses params, query, body, and headers in one call.
110-
- `validateSchema(schema, data)` — for ad-hoc validation against a contract schema or primitive.
111+
- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure.
111112
- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`.
112113
- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`.
113114
- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`.
@@ -119,19 +120,17 @@ import { createLogger } from '@sim/logger'
119120
import type { NextRequest } from 'next/server'
120121
import { NextResponse } from 'next/server'
121122
import { createFolderContract } from '@/lib/api/contracts/folders'
122-
import { parseRequest, validationErrorResponseFromError } from '@/lib/api/server'
123+
import { parseRequest } from '@/lib/api/server'
123124
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
124125

125126
const logger = createLogger('FoldersAPI')
126127

127128
export const POST = withRouteHandler(async (request: NextRequest) => {
128-
try {
129-
const { body } = await parseRequest(createFolderContract, request)
130-
logger.info('Creating folder', { workspaceId: body.workspaceId })
131-
return NextResponse.json({ ok: true })
132-
} catch (error) {
133-
return validationErrorResponseFromError(error)
134-
}
129+
const parsed = await parseRequest(createFolderContract, request, {})
130+
if (!parsed.success) return parsed.response
131+
const { body } = parsed.data
132+
logger.info('Creating folder', { workspaceId: body.workspaceId })
133+
return NextResponse.json({ ok: true })
135134
})
136135
```
137136

apps/sim/app/(landing)/components/contact/contact-form.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import { useMutation } from '@tanstack/react-query'
77
import Link from 'next/link'
88
import { Combobox, Input, Textarea } from '@/components/emcn'
99
import { Check } from '@/components/emcn/icons'
10-
import { flattenFieldErrors } from '@/lib/api/contracts/primitives'
11-
import { getEnv } from '@/lib/core/config/env'
12-
import { captureClientEvent } from '@/lib/posthog/client'
1310
import {
1411
CONTACT_TOPIC_OPTIONS,
1512
type ContactRequestPayload,
1613
contactRequestSchema,
17-
} from '@/app/(landing)/components/contact/consts'
14+
} from '@/lib/api/contracts/contact'
15+
import { flattenFieldErrors } from '@/lib/api/contracts/primitives'
16+
import { getEnv } from '@/lib/core/config/env'
17+
import { captureClientEvent } from '@/lib/posthog/client'
1818
import { LandingField } from '@/app/(landing)/components/forms/landing-field'
1919

2020
type ContactField = keyof ContactRequestPayload

apps/sim/app/(landing)/components/demo-request/demo-request-modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import {
1414
Textarea,
1515
} from '@/components/emcn'
1616
import { Check } from '@/components/emcn/icons'
17-
import { flattenFieldErrors } from '@/lib/api/contracts/primitives'
18-
import { captureClientEvent } from '@/lib/posthog/client'
1917
import {
2018
DEMO_REQUEST_COMPANY_SIZE_OPTIONS,
2119
type DemoRequestPayload,
2220
demoRequestSchema,
23-
} from '@/app/(landing)/components/demo-request/consts'
21+
} from '@/lib/api/contracts/demo-requests'
22+
import { flattenFieldErrors } from '@/lib/api/contracts/primitives'
23+
import { captureClientEvent } from '@/lib/posthog/client'
2424
import { LandingField } from '@/app/(landing)/components/forms/landing-field'
2525

2626
interface DemoRequestModalProps {

apps/sim/app/api/academy/certificates/route.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { and, eq } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
77
import { getCourseById } from '@/lib/academy/content'
88
import type { CertificateMetadata } from '@/lib/academy/types'
9-
import { issueAcademyCertificateBodySchema } from '@/lib/api/contracts'
10-
import { validateSchema } from '@/lib/api/server'
9+
import { issueAcademyCertificateContract } from '@/lib/api/contracts'
10+
import { parseRequest } from '@/lib/api/server'
1111
import { getSession } from '@/lib/auth'
1212
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
1313
import { RateLimiter } from '@/lib/core/rate-limiter'
@@ -43,11 +43,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
4343
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
4444
}
4545

46-
const body = await req.json()
47-
const parsed = validateSchema(issueAcademyCertificateBodySchema, body)
46+
const parsed = await parseRequest(issueAcademyCertificateContract, req, {})
4847
if (!parsed.success) return parsed.response
4948

50-
const { courseId, completedLessonIds } = parsed.data
49+
const { courseId, completedLessonIds } = parsed.data.body
5150

5251
const course = getCourseById(courseId)
5352
if (!course) {

0 commit comments

Comments
 (0)