{"record":{"id":"a71cf6f2ee5960b9","repo":"paperclipai/paperclip","slug":"name-must-be-a-json-object","errorCode":null,"errorMessage":"${name} must be a JSON object","messagePattern":"(.+?) must be a JSON object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/approval.ts","lineNumber":254,"sourceCode":"          printOutput(created, { json: ctx.json });\n        } catch (err) {\n          handleCommandError(err);\n        }\n      }),\n  );\n}\n\nfunction parseCsv(value: string | undefined): string[] | undefined {\n  if (!value) return undefined;\n  const rows = value.split(\",\").map((v) => v.trim()).filter(Boolean);\n  return rows.length > 0 ? rows : undefined;\n}\n\nfunction parseJsonObject(value: string, name: string): Record<string, unknown> {\n  try {\n    const parsed = JSON.parse(value) as unknown;\n    if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n      throw new Error(`${name} must be a JSON object`);\n    }\n    return parsed as Record<string, unknown>;\n  } catch (err) {\n    throw new Error(`Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n","sourceCodeStart":236,"sourceCodeEnd":261,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/cli/src/commands/client/approval.ts#L236-L261","documentation":"HTTP 404 with body {\"error\":\"Provider vault not found\"} from PATCH /api/secret-provider-configs/:id, second guard (secrets.ts:463). Earlier in the same handler getAccessibleResource (authz.ts:182-195) already verified the secret provider config exists and is company-accessible; this 404 fires only because svc.updateProviderConfig(id, ...) then returned null - i.e. no row matched the update. That is a check-then-act race: the config was deleted (or concurrently soft-deleted) between the two calls. This route is board-only (assertBoard).","triggerScenarios":"Concurrent DELETE /api/secret-provider-configs/:id winning the race against a PATCH; two board users editing the same vault config where one removes it mid-request; a soft-deleted status change racing the update so the UPDATE statement matches zero rows.","commonSituations":"Config-management tooling doing read-modify-write on vault configs while operators clean up test vaults; double-pane admin UIs with stale forms submitting after another admin removed the vault.","solutions":["Re-fetch GET /api/secret-provider-configs/:id after the 404 - if it is also 404, the vault was genuinely deleted; update your local state.","Serialize vault mutations (one writer at a time, or an admin lock) to avoid delete/update races.","If the vault must exist, recreate it via POST /api/secret-provider-configs and reapply the intended changes.","Treat this 404 on a previously verified ID as a concurrency signal, not a bad-ID bug - do not simply retry the same PATCH forever."],"exampleFix":"// before\nconst existing = await api.get(`/api/secret-provider-configs/${id}`);\nawait api.patch(`/api/secret-provider-configs/${id}`, changes); // 404 despite existing==200\n\n// after\nconst existing = await api.get(`/api/secret-provider-configs/${id}`);\nif (!existing) throw new Error('vault already deleted');\ntry {\n  await api.patch(`/api/secret-provider-configs/${id}`, changes);\n} catch (e) {\n  if (e.status === 404) {\n    // lost a race with a concurrent delete: re-sync and decide\n    await syncVaults();\n    return;\n  }\n  throw e;\n}","handlingStrategy":"validation","validationCode":"async function patchVaultConfig(api: ApiClient, id: string, changes: unknown) {\n  const existing = await api.fetch(`/api/secret-provider-configs/${id}`);\n  if (existing.status === 404) throw new Error(`vault ${id} not found`);\n  const res = await api.fetch(`/api/secret-provider-configs/${id}`, {\n    method: 'PATCH',\n    body: JSON.stringify(changes),\n  });\n  if (res.status === 404) {\n    // lost a race with a concurrent delete\n    await syncVaultList();\n    return null;\n  }\n  return res.json();\n}","typeGuard":"function isApiErrorBody(body: unknown): body is { error: string } {\n  return typeof body === 'object' && body !== null &&\n    typeof (body as Record<string, unknown>).error === 'string';\n}\nconst isVaultNotFound = (b: unknown): boolean => isApiErrorBody(b) && b.error === 'Provider vault not found';","tryCatchPattern":"try {\n  await api.patch(`/api/secret-provider-configs/${id}`, changes);\n} catch (err) {\n  if (err instanceof ApiError && err.status === 404 && isVaultNotFound(err.body)) {\n    const again = await api.get(`/api/secret-provider-configs/${id}`);\n    if (!again) { await resyncVaults(); return; } // deleted concurrently\n    throw err; // transient inconsistency - retry once\n  }\n  throw err;\n}","preventionTips":["Serialize vault config mutations per vault (single writer or admin lock) to kill the TOCTOU window.","Refresh admin forms right before submit; warn when the record changed underneath.","Treat 404-after-verified-exists as a delete race, not a bad ID.","Audit secret_provider_config.removed activity entries to identify the racing actor."],"tags":["http-404","express","secrets","provider-config","race-condition","toctou","paperclip"],"backgroundTag":"http-404-resource-not-found","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}