Skip to content

Commit 9cc0a93

Browse files
committed
fix(release): keep Firecrawl news/image results, refuse unreachable crawl depth, run pinned async versions on their own variables
1 parent 81c10c9 commit 9cc0a93

15 files changed

Lines changed: 120 additions & 122 deletions

File tree

‎.github/CONTRIBUTING.md‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,15 @@ Using clear and consistent commit messages makes it easier for everyone to under
140140

141141
To set up your local development environment:
142142

143-
### Option 1: Using NPM Package (Simplest)
143+
### Option 1: Using the Setup Wizard (Simplest)
144144

145-
The easiest way to run Sim locally is using our NPM package:
145+
The easiest way to run Sim locally is the `sim-setup` wizard, which creates a deployment directory, generates secrets, and starts the published images with Docker Compose:
146146

147147
```bash
148-
npx simstudio
148+
npx sim-setup
149149
```
150150

151-
After running this command, open [http://localhost:3000/](http://localhost:3000/) in your browser.
152-
153-
#### Options
154-
155-
- `-p, --port <port>`: Specify the port to run Sim on (default: 3000)
156-
- `--no-pull`: Skip pulling the latest Docker images
151+
Inside a cloned Sim repository, run `bun run sim-setup` instead to unlock the source-only local development mode.
157152

158153
#### Requirements
159154

‎apps/sim/app/api/cli/auth/poll/route.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { pollCliAuthContract } from '@/lib/api/contracts'
44
import { parseRequest } from '@/lib/api/server'
55
import {
6+
apiKeyCreationErrorStatus,
67
performCreatePersonalApiKey,
78
performCreateWorkspaceApiKey,
89
} from '@/lib/api-key/orchestration'
@@ -83,7 +84,7 @@ async function mintForGrant(
8384
if (!result.success || !result.key) {
8485
return {
8586
ok: false,
86-
status: result.errorCode === 'conflict' ? 409 : 500,
87+
status: apiKeyCreationErrorStatus(result.errorCode),
8788
message: result.error ?? 'Failed to generate API key',
8889
}
8990
}

‎apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export const GET = withRouteHandler(
160160
}
161161
) => {
162162
const { workflowId: requestedWorkflowId } = await context.params
163-
const access = await validateWorkflowAccess(request, requestedWorkflowId, false)
163+
const access = await validateWorkflowAccess(request, requestedWorkflowId)
164164
if (access.error) {
165165
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
166166
}

‎apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const GET = withRouteHandler(
2323
const { id: workflowId, executionId } = parsed.data.params
2424
const { includeOutput, selectedOutputs } = parsed.data.query
2525

26-
const access = await validateWorkflowAccess(request, workflowId, false)
26+
const access = await validateWorkflowAccess(request, workflowId)
2727
if (access.error) {
2828
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
2929
}

‎apps/sim/app/api/workflows/[id]/paused/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const GET = withRouteHandler(
1414
if (!parsed.success) return parsed.response
1515
const { id: workflowId } = parsed.data.params
1616

17-
const access = await validateWorkflowAccess(request, workflowId, false)
17+
const access = await validateWorkflowAccess(request, workflowId)
1818
if (access.error) {
1919
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
2020
}

‎apps/sim/app/api/workflows/middleware.test.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Tests for workflow access middleware — focused on the workspace-scoped
3-
* API key boundary check in the `requireDeployment=false` branch.
3+
* API key boundary check.
44
*/
55

66
import {
@@ -13,10 +13,6 @@ import { NextRequest } from 'next/server'
1313
import { beforeEach, describe, expect, it, vi } from 'vitest'
1414

1515
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
16-
vi.mock('@/lib/api-key/service', () => ({
17-
authenticateApiKeyFromHeader: vi.fn(),
18-
updateApiKeyLastUsed: vi.fn(),
19-
}))
2016

2117
import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages'
2218
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
@@ -25,7 +21,7 @@ function makeRequest() {
2521
return new NextRequest(new URL('https://example.com/api/workflows/wf-1'))
2622
}
2723

28-
describe('validateWorkflowAccess (requireDeployment=false)', () => {
24+
describe('validateWorkflowAccess', () => {
2925
beforeEach(() => {
3026
workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({
3127
id: 'wf-1',
@@ -48,7 +44,7 @@ describe('validateWorkflowAccess (requireDeployment=false)', () => {
4844
workspaceId: 'ws-B',
4945
})
5046

51-
const result = await validateWorkflowAccess(makeRequest(), 'wf-1', false)
47+
const result = await validateWorkflowAccess(makeRequest(), 'wf-1')
5248

5349
expect(result.error).toEqual({
5450
message: WORKSPACE_KEY_SCOPE_DENIED,
@@ -70,7 +66,7 @@ describe('validateWorkflowAccess (requireDeployment=false)', () => {
7066
message: 'Access denied',
7167
})
7268

73-
const result = await validateWorkflowAccess(makeRequest(), 'wf-1', false)
69+
const result = await validateWorkflowAccess(makeRequest(), 'wf-1')
7470

7571
expect(result.error).toEqual({ message: 'Access denied', status: 403 })
7672
})

‎apps/sim/app/api/workflows/middleware.ts‎

Lines changed: 28 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { createLogger } from '@sim/logger'
22
import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow'
33
import type { NextRequest } from 'next/server'
44
import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages'
5-
import {
6-
type ApiKeyAuthResult,
7-
authenticateApiKeyFromHeader,
8-
updateApiKeyLastUsed,
9-
} from '@/lib/api-key/service'
105
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
116
import { getWorkflowById } from '@/lib/workflows/utils'
127

@@ -20,8 +15,7 @@ export interface ValidationResult {
2015

2116
export async function validateWorkflowAccess(
2217
request: NextRequest,
23-
workflowId: string,
24-
requireDeployment = true
18+
workflowId: string
2519
): Promise<ValidationResult> {
2620
try {
2721
const workflow = await getWorkflowById(workflowId)
@@ -44,95 +38,40 @@ export async function validateWorkflowAccess(
4438
}
4539
}
4640

47-
if (!requireDeployment) {
48-
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
49-
if (!auth.success || !auth.userId) {
50-
return {
51-
error: {
52-
message: auth.error || 'Unauthorized',
53-
status: 401,
54-
},
55-
}
56-
}
57-
58-
if (auth.apiKeyType === 'workspace' && auth.workspaceId !== workflow.workspaceId) {
59-
return {
60-
error: {
61-
message: WORKSPACE_KEY_SCOPE_DENIED,
62-
status: 403,
63-
},
64-
}
65-
}
66-
67-
const authorization = await authorizeWorkflowByWorkspacePermission({
68-
workflowId,
69-
userId: auth.userId,
70-
action: 'read',
71-
})
72-
if (!authorization.allowed) {
73-
return {
74-
error: {
75-
message: authorization.message || 'Access denied',
76-
status: authorization.status,
77-
},
78-
}
41+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
42+
if (!auth.success || !auth.userId) {
43+
return {
44+
error: {
45+
message: auth.error || 'Unauthorized',
46+
status: 401,
47+
},
7948
}
80-
81-
return { workflow, auth }
8249
}
8350

84-
if (requireDeployment) {
85-
if (!workflow.isDeployed) {
86-
return {
87-
error: {
88-
message: 'Workflow is not deployed',
89-
status: 403,
90-
},
91-
}
92-
}
93-
94-
let apiKeyHeader = null
95-
for (const [key, value] of request.headers.entries()) {
96-
if (key.toLowerCase() === 'x-api-key' && value) {
97-
apiKeyHeader = value
98-
break
99-
}
100-
}
101-
102-
if (!apiKeyHeader) {
103-
return {
104-
error: {
105-
message: 'Unauthorized: API key required',
106-
status: 401,
107-
},
108-
}
109-
}
110-
111-
let validResult: ApiKeyAuthResult | null = null
112-
113-
const workspaceResult = await authenticateApiKeyFromHeader(apiKeyHeader, {
114-
workspaceId: workflow.workspaceId as string,
115-
keyTypes: ['workspace', 'personal'],
116-
})
117-
118-
if (workspaceResult.success) {
119-
validResult = workspaceResult
120-
}
121-
122-
if (!validResult) {
123-
return {
124-
error: {
125-
message: 'Unauthorized: Invalid API key',
126-
status: 401,
127-
},
128-
}
51+
if (auth.apiKeyType === 'workspace' && auth.workspaceId !== workflow.workspaceId) {
52+
return {
53+
error: {
54+
message: WORKSPACE_KEY_SCOPE_DENIED,
55+
status: 403,
56+
},
12957
}
58+
}
13059

131-
if (validResult.keyId) {
132-
updateApiKeyLastUsed(validResult.keyId)
60+
const authorization = await authorizeWorkflowByWorkspacePermission({
61+
workflowId,
62+
userId: auth.userId,
63+
action: 'read',
64+
})
65+
if (!authorization.allowed) {
66+
return {
67+
error: {
68+
message: authorization.message || 'Access denied',
69+
status: authorization.status,
70+
},
13371
}
13472
}
135-
return { workflow }
73+
74+
return { workflow, auth }
13675
} catch (error) {
13776
logger.error('Validation error:', { error })
13877
return {

‎apps/sim/background/workflow-execution.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ export async function executeWorkflowJob(
286286
/**
287287
* The version this run executes, loaded once: the core runs this same version's
288288
* blocks, so a deploy landing mid-setup cannot pair one version's variables with
289-
* another's graph.
289+
* another's graph. A pinned version runs with only its own snapshot, as a sync run
290+
* of it does, never the editor's current variables.
290291
*/
291292
const pinnedDeploymentVersionId = payload.pinnedDeploymentVersionId
292293
const deployedState = await withDatabaseReadRetry(
@@ -328,7 +329,9 @@ export async function executeWorkflowJob(
328329
metadata,
329330
workflow,
330331
payload.input,
331-
resolveDeployedRunVariables(deployedState, workflow.variables),
332+
pinnedDeploymentVersionId
333+
? (deployedState.variables ?? {})
334+
: resolveDeployedRunVariables(deployedState, workflow.variables),
332335
[]
333336
)
334337

‎apps/sim/lib/integrations/credential-display.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { CANONICAL_SERVICE_ACCOUNT_SLUGS } from '@/lib/integrations/oauth-servic
2222
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
2323
import type { OAuthProvider, OAuthServiceConfig } from '@/lib/oauth/types'
2424
import { getServiceConfigByProviderId, parseProvider } from '@/lib/oauth/utils'
25+
import { resolveAccessControlBlockType } from '@/lib/permission-groups/integration-allowlist'
2526

2627
/**
2728
* Above this many covered integrations the subtitle states a count instead of
@@ -226,7 +227,7 @@ export function resolveCredentialDisplay(credential: DisplayableCredential): Cre
226227
return {
227228
service: null,
228229
integration,
229-
blockType: 'gitlab',
230+
blockType: resolveAccessControlBlockType('gitlab'),
230231
icon: GitlabIcon,
231232
familyName: null,
232233
coveredIntegrations: integration ? [integration] : [],

‎apps/sim/lib/workspaces/api/route-policies.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import { WorkspaceBusyError } from '@/lib/workspaces/permissions/management-stor
99
import { WorkspaceBillingAccountRemovalError } from '@/lib/workspaces/utils'
1010
import { v2CaughtOrchestrationError, v2Error } from '@/app/api/v2/lib/response'
1111

12+
function isWorkspaceBusyError(error: unknown): error is WorkspaceBusyError {
13+
return error instanceof WorkspaceBusyError
14+
}
15+
1216
/**
1317
* A caller naming a workspace it cannot reach must not be able to tell that
1418
* refusal apart from a workspace that does not exist. Both answer
1519
* `404 "Workspace not found"` — the message the unknown-workspace path in
1620
* `get-public-workspace` and `list-public-workspace-members` already uses, so
1721
* the two responses are byte-identical.
1822
*/
19-
function isWorkspaceBusyError(error: unknown): error is WorkspaceBusyError {
20-
return error instanceof WorkspaceBusyError
21-
}
22-
2323
export const v2WorkspaceErrorPolicies = {
2424
concealWorkspaceAuthorization: createV2ResourceConcealmentPolicy({
2525
notFoundMessage: 'Workspace not found',

0 commit comments

Comments
 (0)