Skip to content

Commit 84b8719

Browse files
committed
merge: sync organization search with staging
Preserve organization invitation and navigation behavior alongside staging OAuth and provenance changes. Enforce the OAuth app capability for organization-scoped reads, with regression coverage. Keep staging migration history intact, regenerate snapshots 0326-0328, and retain the reviewed search, approval, and connected-account SQL unchanged. Update migration fixtures and documentation references.
2 parents df7b170 + 13bdb0d commit 84b8719

322 files changed

Lines changed: 42428 additions & 5733 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/v2-api-conventions/SKILL.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ Order matters because each layer is checked against the one before it.
149149
3. **Route** with `defineV2JsonRoute`, declaring `contract`, `auth: v2ApiKeyAuth`, `operation`, `rateLimit`, `errorPolicy`, `mapInput`, `useCase`, `present`. Auth and rate limiting run before parsing.
150150
4. **OpenAPI description** in `lib/api/contracts/v2/openapi/<domain>.ts`, then `bun run generate:openapi`. A description that claims behaviour the route does not have is the same class of bug as a wrong schema.
151151

152+
### Public descriptions
153+
154+
Use the [API description conventions](../../../apps/sim/lib/api/contracts/v2/openapi/README.md) when writing or auditing endpoint and field descriptions. Keep a short action-and-resource summary; use the description for behavior that changes the caller's choice, input, interpretation, or next action. Ordinary operations usually need one to three sentences, with no mandatory minimum.
155+
156+
Keep archive versus permanent-delete behavior, replacement versus partial-update semantics, partial success, retry safety, redaction, and asynchronous completion explicit. Verify these claims against the implementation. Describe observable behavior without exposing storage formats, locking mechanisms, internal identifiers, deployment architecture, or implementation history unless that detail changes how the caller must use the API.
157+
158+
Reuse wording across resource families when behavior matches: “Omitted fields remain unchanged,” “Archive,” and “permanently delete.” Prefer “during the request” or “asynchronously” to “settled inline.” Preserve real semantic differences; do not standardize them away.
159+
160+
Put field-specific rules in the source schema and reuse shared authentication and pagination wording. Shared schema descriptions also feed CLI help, so refer to related operation names rather than HTTP paths. Regenerate OpenAPI, CLI metadata, and CLI docs after changing their source descriptions; never hand-edit generated output.
161+
152162
## Rule 6 — a transient failure says when to come back
153163

154164
A response the caller is *expected* to retry must say how long to wait. Two statuses qualify, and both are wired:
@@ -186,11 +196,14 @@ Audited against the primary specs and against Stripe, GitHub, and Google's AIPs.
186196

187197
## Idempotency: at-most-once, not replay
188198

189-
`POST /workflows/{id}/execute` accepts `X-Run-Id`, a caller-supplied run identifier claimed through the `idempotency_key` table (`execution-id-claim.ts`). It is a **uniqueness claim, not an idempotency key**, and the distinction is deliberate and already published in the operation description:
199+
`POST /workflows/{id}/execute` accepts `X-Run-Id` from API-key and OAuth callers; anonymous requests ignore it. It is a **uniqueness claim, not an idempotency key**:
200+
201+
- An available ID is claimed before execution starts.
202+
- An already claimed ID returns **409** with `error.details.code: "RUN_ID_CONFLICT"`, the run id in `error.details.runId`, and an `X-Run-Id` response header. It never replays the earlier run's result. Get Workflow Run can retrieve an existing run, but a claim does not guarantee a retrievable run.
203+
- IDs of runs that started remain reserved after their execution logs are deleted.
204+
- An ambiguous enqueue can retain the claim indefinitely without creating a retrievable run. A **409** followed by **404** is an unresolved outcome, not proof that execution never started or that the ID will become reusable.
190205

191-
- First use wins and runs.
192-
- Any reuse returns **409** with `error.details.code: "RUN_ID_CONFLICT"`, the run id in `error.details.runId`, and an `X-Run-Id` response header. It never replays the earlier run's result — the client recovers it by polling the runs resource.
193-
- Claims are durable tombstones, so deleting execution logs cannot make an id reusable.
206+
For an uncertain execution outcome, reuse the same run ID if retrying and check Get Workflow Run. Do not promise polling will eventually find a run. If the outcome cannot be verified, do not automatically restart with a fresh ID or an omitted header: either can start another execution. Failures before a run starts can release the claim, so phrase the conflict rule as an ID that is already claimed.
194207

195208
That makes the money path safe against double-execution **for callers that opt in**. What it is not: a Stripe-style `Idempotency-Key` that stores and replays the original status and body. Building that means a request fingerprint, a retention window, an in-flight-vs-completed distinction (the expired IETF draft would have these be 422 and 409 respectively), and somewhere to put a large synchronous execution body. It is a designed piece of work, not an increment — do not half-build it by aliasing the header name, which would invite clients written against Stripe semantics to treat our 409 as a hard failure.
196209

.github/workflows/test-build.yml

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ permissions:
99

1010
jobs:
1111
oauth-postgres:
12-
name: OAuth PostgreSQL (${{ matrix.provision }})
12+
name: PostgreSQL integration (${{ matrix.provision }})
1313
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
1414
timeout-minutes: 15
1515
strategy:
@@ -22,19 +22,18 @@ jobs:
2222
env:
2323
POSTGRES_USER: postgres
2424
POSTGRES_PASSWORD: postgres
25-
POSTGRES_DB: sim_oauth
25+
POSTGRES_DB: sim_auth_scim
2626
ports:
2727
- 5432:5432
2828
options: >-
29-
--health-cmd "pg_isready -U postgres -d sim_oauth"
29+
--health-cmd "pg_isready -U postgres -d sim_auth_scim"
3030
--health-interval 5s
3131
--health-timeout 5s
3232
--health-retries 10
3333
env:
34-
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_oauth
35-
OAUTH_TOKEN_FAMILY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_oauth
34+
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
35+
OAUTH_TOKEN_FAMILY_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
3636
BETTER_AUTH_SECRET: oauth-postgres-ci-secret-at-least-32-characters
37-
OAUTH_PROVIDER_ENABLED: 'true'
3837
NEXT_PUBLIC_APP_URL: https://test.sim.ai
3938
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
4039

@@ -73,14 +72,86 @@ jobs:
7372
working-directory: packages/db
7473
run: bun run db:migrate
7574

76-
- name: Verify provider issuance and token lifecycle in PostgreSQL
75+
- name: Verify OAuth lifecycle and SCIM membership guards in PostgreSQL
7776
working-directory: apps/sim
7877
run: >-
7978
bunx vitest run
8079
lib/auth/oauth-token-family.postgres.test.ts
8180
lib/auth/oauth-provider-lifecycle.postgres.test.ts
8281
app/api/auth/oauth2/token/route.postgres.test.ts
8382
lib/auth/sim-auth-adapter.test.ts
83+
ee/scim/lib/managed-membership.postgres.test.ts
84+
lib/auth/sso/application/admit-sso-user.postgres.test.ts
85+
86+
- name: Verify SCIM and administration over real HTTP
87+
working-directory: apps/sim
88+
env:
89+
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3017
90+
BETTER_AUTH_URL: http://127.0.0.1:3017
91+
NEXT_PUBLIC_FORCE_HOSTED: 'true'
92+
BILLING_ENABLED: 'true'
93+
NEXT_PUBLIC_BILLING_ENABLED: 'true'
94+
ENTERPRISE_ENABLED: 'true'
95+
NEXT_PUBLIC_ENTERPRISE_ENABLED: 'true'
96+
SCIM_ENABLED: 'true'
97+
NEXT_PUBLIC_SCIM_ENABLED: 'true'
98+
SSO_ENABLED: 'true'
99+
NEXT_PUBLIC_SSO_ENABLED: 'true'
100+
ORGANIZATIONS_ENABLED: 'true'
101+
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: 'true'
102+
INTERNAL_API_SECRET: scim-http-ci-local-secret-at-least-32-characters
103+
DB_TX_TRIPWIRE: throw
104+
DISABLE_TELEMETRY: 'true'
105+
NEXT_TELEMETRY_DISABLED: '1'
106+
NEXT_PUBLIC_CHAT_DISABLED: 'true'
107+
run: |
108+
server_log="$RUNNER_TEMP/scim-next.log"
109+
node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3017 > "$server_log" 2>&1 &
110+
server_pid=$!
111+
finish() {
112+
kill "$server_pid" 2>/dev/null || true
113+
wait "$server_pid" 2>/dev/null || true
114+
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$RUNNER_TEMP/scim-http-status.log"
115+
}
116+
trap finish EXIT
117+
deadline=$((SECONDS + 120))
118+
until curl --fail --silent --max-time 3 http://127.0.0.1:3017/api/health > /dev/null; do
119+
if ! kill -0 "$server_pid" 2>/dev/null; then
120+
echo 'Local SCIM app exited during startup.'
121+
exit 1
122+
fi
123+
if [ "$SECONDS" -ge "$deadline" ]; then
124+
echo 'Local SCIM app did not become ready within 120 seconds.'
125+
exit 1
126+
fi
127+
sleep 2
128+
done
129+
SCIM_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
130+
SCIM_E2E_DATABASE_URL="$DATABASE_URL" \
131+
SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
132+
SCIM_E2E_REPORT_PATH="$RUNNER_TEMP/scim-e2e-report.json" \
133+
bun run test:scim:e2e
134+
135+
- name: Upload SCIM failure report and HTTP status log
136+
if: failure()
137+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
138+
with:
139+
name: scim-failure-${{ matrix.provision }}
140+
path: |
141+
${{ runner.temp }}/scim-e2e-report.json
142+
${{ runner.temp }}/scim-http-status.log
143+
if-no-files-found: ignore
144+
retention-days: 7
145+
146+
- name: Verify durable provenance bindings and concurrent memory writes
147+
working-directory: apps/sim
148+
env:
149+
TABLE_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
150+
MEMORY_PROVENANCE_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
151+
run: >-
152+
bunx vitest run
153+
lib/table/rows/secret-provenance.postgres.test.ts
154+
lib/memory/message-provenance.postgres.test.ts
84155
85156
test-build:
86157
name: Lint and Test

apps/docs/content/docs/cli/authentication.mdx

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,35 @@ description: Sign in from the terminal, authenticate CI with an API key, and kee
55

66
import { Callout } from 'fumadocs-ui/components/callout'
77

8-
`sim login` signs you in through your browser and stores a short-lived login
9-
that renews itself and can be revoked at any time. In CI you supply an API key
10-
through the environment instead.
8+
`sim login` signs you in through your browser. It prefers OAuth, which stores a
9+
short-lived login that renews itself, and selects API-key pairing for remote
10+
terminals or servers without OAuth support. In CI you supply an existing API
11+
key through the environment instead.
1112

1213
## Signing in
1314

1415
```bash
1516
sim login
1617
```
1718

18-
The CLI opens your browser on Sim's sign-in page, then on a consent page that
19+
Choose a method explicitly when the credential type matters:
20+
21+
```bash
22+
sim login --method oauth
23+
sim login --method api-key
24+
```
25+
26+
`--method oauth` requires a server with OAuth support and authentication enabled;
27+
it never falls back to an API key. Explicit OAuth selection also overrides
28+
SSH/headless detection; your browser still needs to reach the CLI's
29+
local callback. `--method api-key` uses pairing-code approval to create a new
30+
permanent API key. To supply an existing key, set `SIM_API_KEY` instead.
31+
32+
API-key pairing requires a server that supports `platform` API keys. Upgrade
33+
older deployments that only issue `copilot` keys before starting login; those
34+
keys cannot authenticate the platform CLI.
35+
36+
OAuth login opens your browser on Sim's sign-in page, then on a consent page that
1937
names the Sim CLI and what it will be able to do. Approve, and the browser hands
2038
control back to the terminal:
2139

@@ -46,21 +64,21 @@ your login.
4664

4765
| Option | What it does |
4866
| --- | --- |
49-
| `--no-browser` | Print the URL instead of opening a browser |
50-
| `--browserless` | Use the pairing-code handoff instead (see below) |
67+
| `--method <method>` | `oauth` requires OAuth login; `api-key` creates a permanent key through pairing. Auto-selects when omitted |
68+
| `--no-browser` | Print the approval URL without opening it; works with either method |
5169
| `--read-only` | Ask only for permission to read, never to change anything |
5270
| `--callback-port <port>` | Pin the loopback callback port, primarily for an SSH session that forwards the same fixed port |
53-
| `--scope <scope>` | Key space for the pairing-code handoff. Only `copilot` changes anything, and it forces that flow |
5471
| `-y, --yes` | Overwrite an existing API-key profile without prompting |
5572

5673
### Over SSH or in a container
5774

58-
The browser login needs your browser to reach a listener on the machine running
75+
OAuth login needs your browser to reach a listener on the machine running
5976
`sim`. When it cannot — an SSH session, a dev container, a remote box — use the
60-
pairing-code handoff, which the CLI selects automatically in an SSH session:
77+
API-key pairing flow. The CLI selects it automatically in an SSH session when
78+
no method or callback port is specified:
6179

6280
```bash
63-
sim login --browserless
81+
sim login --method api-key --no-browser
6482
```
6583

6684
The terminal prints a pairing code and a URL you can open on any device:
@@ -85,16 +103,18 @@ approving. That check is what binds the approval to your terminal.
85103

86104
The handoff issues a permanent personal API key rather than a renewing login,
87105
so revoke it under **Settings → API keys** when you are done with that machine.
88-
It is also the path for a deployment that predates OAuth sign-in, or one with
89-
the provider switched off; the CLI detects that and falls back on its own.
106+
It also works when OAuth is unavailable or switched off, provided the server
107+
supports platform API-key pairing. When `--method` is omitted, the CLI checks
108+
OAuth availability and selects pairing if unavailable; that discovery does not
109+
verify pairing compatibility. An explicit `--method oauth` fails in that case.
90110

91-
`--read-only` and `--callback-port` belong to the browser login and have no
111+
`--read-only` and `--callback-port` belong to OAuth login and have no
92112
meaning here, so combining either with the handoff stops the login rather than
93113
storing a credential you did not ask for. If your SSH session forwards a port
94-
from the remote loopback interface to the browser's machine, pass that same
95-
`--callback-port <port>` on its own. An ordinary container port publication
114+
from the remote loopback interface to the browser's machine, use
115+
`--method oauth --callback-port <port>` with that port. An ordinary container port publication
96116
cannot reach a listener bound to the container's own loopback interface; use
97-
`--browserless` there.
117+
`--method api-key` there.
98118

99119
### Picking a workspace
100120

@@ -160,7 +180,7 @@ authentication profile entirely is refused until its workspace profiles are
160180
removed, so it cannot leave dangling references.
161181

162182
<Callout type="warn">
163-
For a login created with `--browserless`, `sim logout` removes the API key from
183+
For a login created with `--method api-key`, `sim logout` removes the API key from
164184
disk but does **not** revoke it. Revoke keys under **Settings → API keys**.
165185
</Callout>
166186

@@ -237,13 +257,11 @@ Save it to avoid repeating the flag:
237257
sim configure --set-endpoint http://localhost:3000 --profile local
238258
```
239259

240-
A deployment offers OAuth sign-in when its global `oauth-provider` feature flag
241-
is enabled. With AppConfig, enable it in the existing `feature-flags` document
242-
using `"oauth-provider": { "enabled": true }`. When AppConfig is disabled or no
243-
AppConfig document has been loaded, `OAUTH_PROVIDER_ENABLED=true` supplies the fallback.
244-
With the provider off, the CLI uses the pairing-code handoff; `DISABLE_AUTH=true`
245-
always forces OAuth off. Operators must apply the database migration and drain
246-
older app instances before enabling it. See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim).
260+
OAuth sign-in is available by default when server authentication is enabled.
261+
`DISABLE_AUTH=true` disables OAuth. Older servers without OAuth support use the
262+
pairing-code handoff. Keep the database schema current and drain app instances
263+
that predate the OAuth token-family lifecycle before accepting OAuth traffic.
264+
See [Sign in with Sim](/platform/self-hosting/authentication#sign-in-with-sim).
247265

248266
## Where the login is stored
249267

@@ -279,5 +297,5 @@ a copied token from surviving an ambiguous refresh.
279297
## Organization audit logs
280298

281299
`sim audit-logs` requires a **personal** credential — an OAuth login, or the
282-
personal API key `sim login --browserless` issues. A workspace-scoped key cannot
300+
personal API key `sim login --method api-key` issues. A workspace-scoped key cannot
283301
read organization-level audit logs.

apps/docs/content/docs/cli/blocks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ sim blocks list [options]
3838
| `--search <value>` | No | Case-insensitive substring match against the block id, name, and description. |
3939
| `--category <value>` | No | Restrict to one toolbar category. Accepted values: `blocks`, `tools`, `triggers`. |
4040
| `--capability <value>` | No | Restrict to blocks that can start a workflow — the `triggers` category, blocks declaring `triggerAllowed`, and blocks with trigger-mode fields. Accepted values: `trigger`. |
41-
| `--source <value>` | No | Restrict to shipped blocks or to this workspaces deployed custom blocks. Accepted values: `builtin`, `custom`. |
41+
| `--source <value>` | No | Restrict to built-in blocks or this workspace's deployed custom blocks. Accepted values: `builtin`, `custom`. |
4242
| `--sort-by <value>` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `id`, `name`, `category`. |
4343
| `--sort-order <value>` | No | Sort direction. Accepted values: `asc`, `desc`. |
4444
| `--limit <n>` | No | Maximum items to return (0 for everything). Defaults to `100`. |

apps/docs/content/docs/cli/commands.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@ sim login [options]
6464

6565
| Option | Required | Description |
6666
| --- | --- | --- |
67-
| `--scope <scope>` | No | Key space for the pairing-code handoff; only "copilot" changes anything, and it forces that flow. Defaults to `platform`. |
68-
| `--no-browser` | No | Print the URL instead of opening a browser. |
69-
| `--browserless` | No | Use the pairing-code handoff for a terminal whose browser cannot reach it (SSH, containers). |
67+
| `--method <method>` | No | Credential to obtain: oauth requires OAuth support; api-key creates a permanent key through pairing (auto-selects when omitted). Accepted values: `oauth`, `api-key`. |
68+
| `--no-browser` | No | Print the approval URL without opening it (either login method). |
7069
| `--read-only` | No | Ask only for permission to read, never to change anything. |
7170
| `--callback-port <port>` | No | Pin the local port the browser returns to. |
7271
| `-y, --yes` | No | Overwrite an existing API-key profile without prompting. |

apps/docs/content/docs/cli/files.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ sim files list [options]
286286
| Option | Required | Description |
287287
| --- | --- | --- |
288288
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
289-
| `--recursive` | No | Whether the folder filter includes files in subfolders. Defaults to true when a search is set, false otherwise, so listing a folder shows that folder while searching one looks through everything in it. Ignored when no folder filter is set, which already spans the workspace. |
289+
| `--recursive` | No | Include subfolders in the folder filter. Defaults to true when searching and false otherwise. Ignored without a folder filter. |
290290
| `--no-recursive` | No | Send --recursive as false. |
291291
| `--scope <value>` | No | Which lifecycle set to list: `active` (default) for live files, `archived` for files a delete soft-deleted. `folderPath` resolves against active folders only, so pairing it with `scope=archived` returns an empty page when the containing folder was archived too. Accepted values: `active`, `archived`. |
292292
| `--search <value>` | No | Case-insensitive substring match against the file name. |

0 commit comments

Comments
 (0)