Skip to content

Commit 95b5cbb

Browse files
committed
docs(v2-api): correct eleven false or misleading spec claims
Structural parity between contracts and specs is CI-enforced; semantic truth is not. These are claims the spec made that the code does not honour. Outright false: - `DELETE /files/{fileId}` said it deletes "the stored bytes". It archives: the row is retained with a deletion timestamp and the bytes are never removed. Restore exists, but only on the internal API, so the description now says so rather than implying v2 offers it. - Execute documented `409 EXECUTION_ID_CONFLICT` in three places. The wire carries `error.code: CONFLICT` with `error.details.code: RUN_ID_CONFLICT`; only v1 ever emitted the documented string. - The files spec claimed every endpoint uses the canonical envelopes while `GET /files/{fileId}` returns octet-stream. - The shared timestamp rule justified itself with a rendering claim that is false — 29 bare-form sites publish `format: date-time` identically. The real difference is runtime validation, so the rule now says that. It was softened rather than enforced: responses are re-parsed, so adding `.datetime()` to a field whose producer can emit a non-ISO string turns a working read into a 500, and that could not be proven for all 29 without a much larger audit. Misleading: - The billing ledger silently defaults to a 30-day window, so a client paginating to `nextCursor: null` believes it has the whole ledger. - Deleting a connector-backed knowledge document does not delete its chunks — the row survives as excluded and the embeddings remain. - `listTables` said "all tables"; it is keyset-paged with a default limit. - `GET /files/{id}/share` omitted the `data: null` never-shared case its own schema and example already declare. - The share PATCH matrix omitted two hard 400s, so following it literally against a never-shared file fails. - Five knowledge operations render a canonical folder path back and can 413 on an oversized tree without carrying the sentence that says so. Also: the upload-control helper was a third implementation of concealment by sniffing `response.status === 403`, which masks workspace-policy denials the canonical helper deliberately preserves. It now uses the shared policy, so those denials keep their 403. And the shared docblock's search-field enumeration was presented as exhaustive while omitting two lists, and its error-envelope claim omitted the two upload data-plane routes that emit a bare `{error: string}` — both now carry the carve-out the CI allowlist already had.
1 parent 378cb1c commit 95b5cbb

18 files changed

Lines changed: 215 additions & 57 deletions

File tree

apps/docs/openapi-v2-billing.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"get": {
102102
"operationId": "listBillingLogs",
103103
"summary": "List Billing Logs",
104-
"description": "List the credit-denominated billing ledger with source filtering and opaque cursor pagination.",
104+
"description": "List the credit-denominated billing ledger with source filtering and opaque cursor pagination. `period` defaults to `30d`, so an unqualified request covers only the last 30 days: paginating to `nextCursor: null` exhausts that window, not the whole ledger. Pass `period=all` for full history, or `period=custom` with `startDate` and `endDate` for a specific range.",
105105
"tags": ["Billing"],
106106
"parameters": [
107107
{

apps/docs/openapi-v2-files-audit.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.1.0",
33
"info": {
44
"title": "Sim API v2 — Files & Audit Logs",
5-
"description": "Version 2 of the Sim REST API for workspace files and organization audit logs. Every endpoint uses the canonical v2 data, cursor-list, and error envelopes. Lists use opaque cursors, and rate-limit state is returned in response headers.",
5+
"description": "Version 2 of the Sim REST API for workspace files and organization audit logs. Lists use opaque cursors, and rate-limit state is returned in response headers. Download File streams raw bytes as `application/octet-stream`; every other response uses the canonical v2 data, cursor-list, or error envelope.",
66
"version": "2.0.0",
77
"contact": {
88
"name": "Sim Support",
@@ -667,7 +667,7 @@
667667
"delete": {
668668
"operationId": "deleteFile",
669669
"summary": "Delete File",
670-
"description": "Delete a workspace file and its stored bytes.",
670+
"description": "Archive a workspace file. This is a soft delete: the row is retained with a deletion timestamp, the file stops appearing in listings and is no longer readable through the API, and its stored bytes are never removed. An archived file can be restored from the workspace Recently Deleted settings; the v2 API exposes no restore operation.",
671671
"tags": ["Files"],
672672
"parameters": [
673673
{
@@ -1216,7 +1216,7 @@
12161216
"get": {
12171217
"operationId": "getFileShare",
12181218
"summary": "Get File Share",
1219-
"description": "Return the current public-share configuration for a file.",
1219+
"description": "Return the nullable current public-share configuration for a file. A file that has never been shared returns `data: null` rather than a 404; a share that was created and later disabled is still returned, with `isActive: false`.",
12201220
"tags": ["Files"],
12211221
"parameters": [
12221222
{
@@ -1292,7 +1292,7 @@
12921292
"patch": {
12931293
"operationId": "upsertFileShare",
12941294
"summary": "Enable or Disable File Share",
1295-
"description": "Create or partially update a server-tokenized public share. Only isActive is required, and an omitted authType keeps the stored auth mode. What happens to password and allowedEmails depends on the resulting mode, because enabling a share always rewrites the credentials the chosen mode does not use: 'public' clears the stored password and empties allowedEmails; 'password' keeps the stored password when password is omitted but empties allowedEmails; 'email' and 'sso' clear the stored password and keep the stored allowedEmails when the field is omitted. Only disabling with isActive false preserves the whole access configuration untouched — it also retains the token, so re-enabling restores the share as it was. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
1295+
"description": "Create or partially update a server-tokenized public share. Only isActive is required, and an omitted authType keeps the stored auth mode. What happens to password and allowedEmails depends on the resulting mode, because enabling a share always rewrites the credentials the chosen mode does not use: 'public' clears the stored password and empties allowedEmails; 'password' keeps the stored password when password is omitted but empties allowedEmails; 'email' and 'sso' clear the stored password and keep the stored allowedEmails when the field is omitted. Only disabling with isActive false preserves the whole access configuration untouched — it also retains the token, so re-enabling restores the share as it was. Two enabling combinations are rejected outright with a 400 instead of being partially applied: 'password' when neither a password is supplied nor one is already stored, and 'email' or 'sso' when the resulting allowedEmails would be empty because none was supplied and none is stored. On a file that has never been shared there is nothing stored to fall back on, so enabling any mode other than 'public' must carry its credential in the same request. A workspace API key cannot call this operation. Because unauthorized resources are concealed, the rejection is reported as `404` rather than `403`; use a personal API key.",
12961296
"tags": ["Files"],
12971297
"parameters": [
12981298
{

apps/docs/openapi-v2-knowledge.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
"post": {
149149
"operationId": "createKnowledgeBase",
150150
"summary": "Create Knowledge Base",
151-
"description": "Create a knowledge base in a workspace with optional folder placement and chunking configuration. An unknown `folderPath` is a 404",
151+
"description": "Create a knowledge base in a workspace with optional folder placement and chunking configuration. An unknown `folderPath` is a 404. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
152152
"tags": ["Knowledge Bases"],
153153
"requestBody": {
154154
"required": true,
@@ -291,7 +291,7 @@
291291
"patch": {
292292
"operationId": "updateKnowledgeBase",
293293
"summary": "Update Knowledge Base",
294-
"description": "Update a knowledge base name, description, chunking configuration, or folder placement.",
294+
"description": "Update a knowledge base name, description, chunking configuration, or folder placement. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
295295
"tags": ["Knowledge Bases"],
296296
"parameters": [
297297
{
@@ -1242,7 +1242,7 @@
12421242
"delete": {
12431243
"operationId": "deleteKnowledgeDocument",
12441244
"summary": "Delete Document",
1245-
"description": "Delete one document and its indexed chunks from a knowledge base.",
1245+
"description": "Remove one document from a knowledge base. What that means depends on the document. A directly uploaded document is deleted outright along with its indexed chunks. A connector-backed document is instead excluded: its row survives, marked excluded and disabled so it stops being searchable and a later connector sync does not re-add it, and its embeddings are not deleted. Either way the document no longer appears in listings or search results.",
12461246
"tags": ["Knowledge Bases"],
12471247
"parameters": [
12481248
{
@@ -1329,7 +1329,7 @@
13291329
"get": {
13301330
"operationId": "listKnowledgeFolders",
13311331
"summary": "List Folders",
1332-
"description": "List folders in the knowledge-base folder tree with filtering and sorting. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch.",
1332+
"description": "List folders in the knowledge-base folder tree with filtering and sorting. The bounded set is returned in one page with `nextCursor` always null; there is no second page to fetch. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
13331333
"tags": ["Knowledge Bases"],
13341334
"parameters": [
13351335
{
@@ -1441,7 +1441,7 @@
14411441
"post": {
14421442
"operationId": "createKnowledgeFolder",
14431443
"summary": "Create Folder",
1444-
"description": "Create a folder in the knowledge-base folder tree.",
1444+
"description": "Create a folder in the knowledge-base folder tree. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
14451445
"tags": ["Knowledge Bases"],
14461446
"requestBody": {
14471447
"required": true,
@@ -1508,7 +1508,7 @@
15081508
"patch": {
15091509
"operationId": "relocateKnowledgeFolder",
15101510
"summary": "Rename or Move Folder",
1511-
"description": "Rename or move a folder and atomically rewrite descendant paths.",
1511+
"description": "Rename or move a folder and atomically rewrite descendant paths. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
15121512
"tags": ["Knowledge Bases"],
15131513
"requestBody": {
15141514
"required": true,

apps/docs/openapi-v2-tables.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"get": {
3737
"operationId": "listTables",
3838
"summary": "List Tables",
39-
"description": "List all tables in a workspace with optional folder filtering, search, sorting, and an opaque cursor envelope. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
39+
"description": "List tables in a workspace with optional folder filtering, search, sorting, and an opaque cursor envelope. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
4040
"tags": ["Tables"],
4141
"parameters": [
4242
{
@@ -123,7 +123,7 @@
123123
],
124124
"responses": {
125125
"200": {
126-
"description": "The tables in the workspace.",
126+
"description": "A page of tables in the workspace.",
127127
"headers": {
128128
"X-RateLimit-Limit": {
129129
"$ref": "#/components/headers/X-RateLimit-Limit"

apps/docs/openapi-v2-workflows.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@
10211021
"post": {
10221022
"operationId": "executeWorkflowV2",
10231023
"summary": "Execute Workflow",
1024-
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 `EXECUTION_ID_CONFLICT` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
1024+
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with `error.details.code: \"RUN_ID_CONFLICT\"` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
10251025
"tags": ["Workflows"],
10261026
"security": [
10271027
{
@@ -1046,9 +1046,9 @@
10461046
"name": "x-run-id",
10471047
"in": "header",
10481048
"required": false,
1049-
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 `EXECUTION_ID_CONFLICT` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
1049+
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 and `error.details.code: \"RUN_ID_CONFLICT\"` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
10501050
"schema": {
1051-
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 `EXECUTION_ID_CONFLICT` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
1051+
"description": "Caller-supplied run identifier, available only to API-key callers. This is a one-shot uniqueness claim, NOT an idempotency key: the first request to use a value starts a run, and any later request reusing it fails with 409 and `error.details.code: \"RUN_ID_CONFLICT\"` instead of replaying the original result. To retry safely, generate a fresh value per attempt and reconcile duplicates yourself, or omit the header and let the server allocate the run identifier.",
10521052
"type": "string",
10531053
"minLength": 1,
10541054
"maxLength": 128,

apps/sim/app/api/v2/files/uploads/[uploadId]/complete/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { v2CompleteFileUploadContract } from '@/lib/api/contracts/v2/files'
22
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
33
import { completeWorkspaceFileUploadOperation } from '@/lib/uploads/upload-session/application'
4+
import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
45
import { fileOperations } from '@/lib/workspace-files/application/operations'
5-
import { toV2FileUpload, v2UploadControlError } from '@/app/api/v2/files/uploads/utils'
6+
import { toV2FileUpload } from '@/app/api/v2/files/uploads/utils'
67

78
export const POST = defineV2JsonRoute({
89
contract: v2CompleteFileUploadContract,
910
auth: v2ApiKeyAuth,
1011
operation: fileOperations.uploadComplete,
1112
rateLimit: v2RateLimits.publicApi,
12-
errorPolicy: { render: v2UploadControlError },
13+
errorPolicy: v2FileErrorPolicies.concealUploadAuthorization,
1314
mapInput: ({ params, query, headers }) => ({
1415
uploadId: params.uploadId,
1516
workspaceId: query.workspaceId,

apps/sim/app/api/v2/files/uploads/[uploadId]/parts/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { v2CreateFileUploadPartUrlsContract } from '@/lib/api/contracts/v2/files'
22
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
33
import { issueWorkspaceFileUploadPartsOperation } from '@/lib/uploads/upload-session/application'
4+
import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
45
import { fileOperations } from '@/lib/workspace-files/application/operations'
5-
import { v2UploadControlError } from '@/app/api/v2/files/uploads/utils'
66

77
export const POST = defineV2JsonRoute({
88
contract: v2CreateFileUploadPartUrlsContract,
99
auth: v2ApiKeyAuth,
1010
operation: fileOperations.uploadParts,
1111
rateLimit: v2RateLimits.publicApi,
12-
errorPolicy: { render: v2UploadControlError },
12+
errorPolicy: v2FileErrorPolicies.concealUploadAuthorization,
1313
mapInput: ({ params, query, headers, body }) => ({
1414
uploadId: params.uploadId,
1515
workspaceId: query.workspaceId,

0 commit comments

Comments
 (0)