Skip to content

Commit f1d0b0f

Browse files
committed
Merge remote-tracking branch 'origin/staging' into fix/release-audit-unique-1110
2 parents 87fc208 + 15cf51d commit f1d0b0f

19 files changed

Lines changed: 159 additions & 122 deletions

File tree

‎.github/CI.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installin
44

55
A PR compares the checked-out GitHub merge commit with its first parent: the actual target revision tested by that merge. The PR event's base SHA can become stale when the target advances. Scoping requires a matching GitHub merge ref and SHA, exactly two merge parents, and a usable target parent; otherwise every check runs. This works with the existing depth-two checkout without fetching older ancestry. Main/staging pushes execute all checks; the parent `ci` workflow skips checks on `dev` pushes, while PRs targeting `dev` still run checks. A manual `checks` run executes all checks unless supplied a full base commit SHA to reproduce a scoped plan. Missing history, an empty diff, unknown files outside the workspace graph, root script/CI changes, or a planner error run every check. Missing selection outputs fail the aggregate gate. The gate also rejects an omitted dependency, any failed/cancelled job, and any skip that its explicit selection flag did not authorize.
66

7-
Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Independent SDK and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip.
7+
Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Standalone package and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip.
88

99
Docs production builds run once and publish an artifact to isolated HTTP, corpus, Chromium, and WebKit jobs. Source checks keep their own checkout because generator verification mutates inputs. Content caches use separate lint/source/build namespaces; generation validates every restore, repairs missing declarations, and removes obsolete modules. The Next build cache keeps its supported incremental behavior. Content and Next caches have size limits, and Next cache files expire after seven days; successful content eviction happens after all consumers finish.
1010

1111
| Change | Expensive checks selected |
1212
| --- | --- |
13-
| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers |
13+
| Standalone package | That workspace's tests and type-check; no app build, services, or browsers |
1414
| App unit test | App test shards and test type check |
1515
| Integration test | Both provision paths; no HTTP/browser job merely for the test edit |
1616
| Workspace UI | Database integration, mobile, SEO and live Electron, plus app tests/build |

‎apps/docs/content/docs/search/gitlab.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ To rotate the PAT, supply its replacement in source settings. A blank token reta
9191

9292
After an admin configures a source in **Non-admin token** mode, a job can [read its revision](/api-reference/organizations/getSearchSourcePermissions) and [replace both CSVs](/api-reference/organizations/replaceSearchSourcePermissions). Copy `SIM_ORG_ID` and `SIM_SOURCE_ID` from the source settings URL: `/o/<organizationId>/settings/integrations/sources/<sourceId>`. Set `SIM_BASE_URL` to your Sim origin (for hosted Sim, `https://www.sim.ai`).
9393

94-
Store an organization admin's [personal Sim API key](/api-reference/authentication) as a [masked, protected GitLab CI variable](https://docs.gitlab.com/ci/variables/) named `SIM_API_KEY`. A personal key inherits its owner's access. This endpoint can only read the revision or replace both CSVs; it cannot change the GitLab PAT, host, or project.
94+
Store an organization admin's [personal Sim API key](/api-reference/authentication) with [**Full access**](/api-reference/authentication#key-access) as a [masked, protected GitLab CI variable](https://docs.gitlab.com/ci/variables/) named `SIM_API_KEY`. A personal key inherits its owner's access; a read-only key can read the revision but not replace the CSVs. This endpoint can only read the revision or replace both CSVs; it cannot change the GitLab PAT, host, or project.
9595

9696
This example runs on a schedule against a protected branch. Replace `./ci/export-gitlab-permissions` with your existing export command. It must write a fresh, complete `users.csv` and `project-permissions.csv` and exit nonzero if it cannot finish.
9797

‎apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ export const RESOURCES_MENU: NavMenu = {
128128
items: [
129129
{
130130
title: 'Docs',
131-
description: 'Guides, SDKs, and API reference',
131+
description: 'Guides, CLI, and API reference',
132132
href: 'https://docs.sim.ai',
133133
external: true,
134134
preview: {
135135
kind: 'docs',
136136
eyebrow: 'Documentation',
137137
title: 'Build with Sim',
138-
details: ['Quickstart', 'SDKs', 'API reference'],
138+
details: ['Quickstart', 'CLI', 'API reference'],
139139
},
140140
},
141141
{

‎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/v2/workspaces/[workspaceId]/members/[userId]/route.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { isUserCredentialPrincipal } from '@sim/auth/principal'
12
import {
23
v2RemoveWorkspaceMemberContract,
34
v2UpdateWorkspaceMemberContract,
45
} from '@/lib/api/contracts/v2/workspaces'
56
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
67
import { presentWorkspaceMember } from '@/lib/api/server/workspace-presenters'
8+
import { captureServerEvent } from '@/lib/posthog/server'
79
import { v2WorkspaceErrorPolicies } from '@/lib/workspaces/api/route-policies'
810
import { updateWorkspaceMember } from '@/lib/workspaces/application/manage-permissions'
911
import {
@@ -22,6 +24,17 @@ export const PATCH = defineV2JsonRoute({
2224
mapInput: ({ params, body }) => ({ ...params, ...body }),
2325
useCase: updateWorkspaceMember,
2426
present: ({ member }) => ({ data: presentWorkspaceMember(member) }),
27+
onSuccess: ({ principal, input, result }) => {
28+
if (!isUserCredentialPrincipal(principal)) return
29+
for (const change of result.changes) {
30+
captureServerEvent(
31+
principal.userId,
32+
'workspace_member_role_changed',
33+
{ workspace_id: input.workspaceId, new_role: change.newRole },
34+
{ groups: { workspace: input.workspaceId } }
35+
)
36+
}
37+
},
2538
})
2639

2740
/** DELETE /api/v2/workspaces/[workspaceId]/members/[userId] — Remove a member, or leave. */
@@ -40,4 +53,13 @@ export const DELETE = defineV2JsonRoute({
4053
organizationMembershipRemoved: result.organizationRemoval,
4154
},
4255
}),
56+
onSuccess: ({ principal, input, result }) => {
57+
if (!isUserCredentialPrincipal(principal)) return
58+
captureServerEvent(
59+
principal.userId,
60+
'workspace_member_removed',
61+
{ workspace_id: input.workspaceId, is_self_removal: result.selfRemoval },
62+
{ groups: { workspace: input.workspaceId } }
63+
)
64+
},
4365
})

‎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 {

0 commit comments

Comments
 (0)