Skip to content

Commit db8fd05

Browse files
committed
Merge branch 'staging' into improvement/invites-copy
2 parents 88c1ba1 + a9c12a2 commit db8fd05

1,430 files changed

Lines changed: 53377 additions & 29497 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: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -578,38 +578,64 @@ tools: {
578578

579579
#### 3. Create Internal API Route
580580

581-
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
581+
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/{service}-tools.ts` (or an existing `internal-tools.ts` / `communication-tools.ts` aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema.
582582

583583
```typescript
584+
// apps/sim/lib/api/contracts/{service}-tools.ts
585+
import { z } from 'zod'
586+
import { defineRouteContract } from '@/lib/api/contracts'
587+
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
588+
589+
export const {service}UploadBodySchema = z.object({
590+
accessToken: z.string(),
591+
file: FileInputSchema.optional().nullable(),
592+
fileContent: z.string().optional().nullable(),
593+
// ... other params
594+
})
595+
596+
export const {service}UploadResponseSchema = z.object({
597+
success: z.boolean(),
598+
output: z.object({ id: z.string(), url: z.string() }).optional(),
599+
error: z.string().optional(),
600+
})
601+
602+
export const {service}UploadContract = defineRouteContract({
603+
method: 'POST',
604+
path: '/api/tools/{service}/upload',
605+
body: {service}UploadBodySchema,
606+
response: { mode: 'json', schema: {service}UploadResponseSchema },
607+
})
608+
609+
export type {Service}UploadBody = z.input<typeof {service}UploadBodySchema>
610+
export type {Service}UploadResponse = z.output<typeof {service}UploadResponseSchema>
611+
```
612+
613+
```typescript
614+
// apps/sim/app/api/tools/{service}/upload/route.ts
584615
import { createLogger } from '@sim/logger'
585616
import { NextResponse, type NextRequest } from 'next/server'
586-
import { z } from 'zod'
617+
import { {service}UploadContract } from '@/lib/api/contracts/{service}-tools'
618+
import { parseRequest } from '@/lib/api/server'
587619
import { checkInternalAuth } from '@/lib/auth/hybrid'
588620
import { generateRequestId } from '@/lib/core/utils/request'
589-
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
621+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
622+
import { type RawFileInput } from '@/lib/uploads/utils/file-schemas'
590623
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
591624
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
592625

593626
const logger = createLogger('{Service}UploadAPI')
594627

595-
const RequestSchema = z.object({
596-
accessToken: z.string(),
597-
file: FileInputSchema.optional().nullable(),
598-
// Legacy field for backwards compatibility
599-
fileContent: z.string().optional().nullable(),
600-
// ... other params
601-
})
602-
603-
export async function POST(request: NextRequest) {
628+
export const POST = withRouteHandler(async (request: NextRequest) => {
604629
const requestId = generateRequestId()
605630

606631
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
607632
if (!authResult.success) {
608633
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
609634
}
610635

611-
const body = await request.json()
612-
const data = RequestSchema.parse(body)
636+
const parsed = await parseRequest({service}UploadContract, request, {})
637+
if (!parsed.success) return parsed.response
638+
const data = parsed.data.body
613639

614640
let fileBuffer: Buffer
615641
let fileName: string
@@ -624,22 +650,20 @@ export async function POST(request: NextRequest) {
624650
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
625651
fileName = userFile.name
626652
} else if (data.fileContent) {
627-
// Legacy: base64 string (backwards compatibility)
628653
fileBuffer = Buffer.from(data.fileContent, 'base64')
629654
fileName = 'file'
630655
} else {
631656
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
632657
}
633658

634-
// Now call external API with fileBuffer
635659
const response = await fetch('https://api.{service}.com/upload', {
636660
method: 'POST',
637661
headers: { Authorization: `Bearer ${data.accessToken}` },
638-
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
662+
body: new Uint8Array(fileBuffer),
639663
})
640664

641665
// ... handle response
642-
}
666+
})
643667
```
644668

645669
#### 4. Update Tool to Use Internal Route

.agents/skills/cleanup/SKILL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ Run each of these skills in order on the specified scope, passing through the sc
2323
6. `/emcn-design-review $ARGUMENTS`
2424

2525
After all skills have run, output a summary of what was found and fixed (or proposed) across all six passes.
26+
27+
## Boundary Audit Guidance
28+
29+
- When removing route-local Zod schemas, replacing raw `fetch(` calls in hooks, or removing `as unknown as X` casts, do not introduce `// boundary-raw-fetch: <reason>` or `// double-cast-allowed: <reason>` annotations to silence the audit. Fix the underlying call instead — adopt a contract from `@/lib/api/contracts/**` and use `requestJson(contract, ...)` from `@/lib/api/client/request`, or refine the type so the double cast is unnecessary.
30+
- Annotations are reserved for legitimate exceptions only: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, external-origin requests, and double casts where no narrower type is available. Each annotation requires a non-empty reason; empty reasons fail `bun run check:api-validation:strict`.

.cursor/rules/sim-architecture.mdc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,16 @@ feature/
5454
- **Create `utils.ts` when** 2+ files need the same helper
5555
- **Check existing sources** before duplicating (`lib/` has many utilities)
5656
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)
57+
58+
## API Contracts
59+
60+
Boundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/` (one file per resource family). Routes and clients consume the same contract — routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types. Domain validators that are not HTTP boundaries (tools, blocks, triggers, connectors, realtime handlers, internal helpers) may still use Zod directly; the contract rule is boundary-only.
61+
62+
- 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>`).
63+
- 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`, `validationErrorResponse`, `getValidationErrorMessage`, `isZodError`). Routes never `import { z } from 'zod'` and never use `instanceof z.ZodError`.
65+
- Clients call `requestJson(contract, ...)` from `apps/sim/lib/api/client/request.ts`; hooks import named type aliases from contracts, never `z.input/z.output`.
66+
- 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.
67+
- `bun run check:api-validation` enforces this policy and must pass on PRs.
68+
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.

.cursor/rules/sim-queries.mdc

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ Never use inline query keys — always use the factory.
3737
- Every `queryFn` must destructure and forward `signal` for request cancellation
3838
- Every query must have an explicit `staleTime`
3939
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
40+
- Same-origin JSON calls must go through `requestJson(contract, ...)` from `@/lib/api/client/request` against the contract in `@/lib/api/contracts/**`
4041

4142
```typescript
42-
async function fetchEntities(workspaceId: string, signal?: AbortSignal) {
43-
const response = await fetch(`/api/entities?workspaceId=${workspaceId}`, { signal })
44-
if (!response.ok) throw new Error('Failed to fetch entities')
45-
return response.json()
43+
import { requestJson } from '@/lib/api/client/request'
44+
import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'
45+
46+
async function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {
47+
const data = await requestJson(listEntitiesContract, {
48+
query: { workspaceId },
49+
signal,
50+
})
51+
return data.entities
4652
}
4753

4854
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
@@ -51,7 +57,7 @@ export function useEntityList(workspaceId?: string, options?: { enabled?: boolea
5157
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
5258
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
5359
staleTime: 60 * 1000,
54-
placeholderData: keepPreviousData, // OK: workspaceId varies
60+
placeholderData: keepPreviousData,
5561
})
5662
}
5763
```
@@ -118,6 +124,12 @@ const handler = useCallback(() => {
118124
}, [data])
119125
```
120126

127+
## Boundary Types
128+
129+
- Hooks must import named type aliases from `@/lib/api/contracts/**` (e.g., `import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'`). Never write `z.input<...>` or `z.output<...>` in hooks.
130+
- Hooks must not `import { z } from 'zod'`. Boundary types come from contract aliases; non-boundary helpers can stay in plain TypeScript.
131+
- For non-contract endpoints (multipart uploads, binary downloads, streaming responses, signed-URL flows, OAuth redirects, external origins), it is OK to keep raw `fetch`. Each legitimate raw `fetch(` call inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` must be preceded by a `// boundary-raw-fetch: <reason>` annotation on the immediately preceding line (up to three non-empty preceding comment lines are tolerated). The reason must be non-empty — empty reasons fail strict mode. The audit script `scripts/check-api-validation-contracts.ts` (`bun run check:api-validation` / `bun run check:api-validation:strict`) enforces this.
132+
121133
## Naming
122134

123135
- **Keys**: `entityKeys`

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ services:
2121
- COPILOT_API_KEY=${COPILOT_API_KEY}
2222
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
2323
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
24-
- NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-http://localhost:3002}
24+
- NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-}
2525
- BUN_INSTALL_CACHE_DIR=/home/bun/.bun/cache
2626
depends_on:
2727
db:

.github/workflows/test-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ jobs:
106106
- name: Enforce monorepo boundaries
107107
run: bun run check:boundaries
108108

109+
- name: API contract boundary audit
110+
run: bun run check:api-validation:strict
111+
109112
- name: Verify realtime prune graph
110113
run: bun run check:realtime-prune
111114

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ i18n.cache
8080
## Claude Code
8181
.claude/launch.json
8282
.claude/worktrees/
83+
.claude/scheduled_tasks.lock

0 commit comments

Comments
 (0)