{"record":{"id":"d3f115463da68552","repo":"paperclipai/paperclip","slug":"file-is-required","errorCode":null,"errorMessage":"--file is required","messagePattern":"--file is required","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/asset.ts","lineNumber":95,"sourceCode":"            printOutput({ ok: true, out: opts.out, bytes: bytes.length }, { json: ctx.json });\n            return;\n          }\n          process.stdout.write(bytes);\n        } catch (err) {\n          handleCommandError(err);\n        }\n      }),\n  );\n}\n\nasync function uploadAsset(\n  apiBase: string,\n  apiKey: string | undefined,\n  path: string,\n  opts: AssetOptions,\n): Promise<unknown> {\n  if (!opts.file?.trim()) {\n    throw new Error(\"--file is required\");\n  }\n  const bytes = await readFile(opts.file);\n  const form = new FormData();\n  form.set(\"file\", new Blob([bytes], { type: inferContentTypeFromPath(opts.file) }), opts.file.split(/[\\\\/]/).pop() ?? \"asset\");\n  if (opts.namespace?.trim()) form.set(\"namespace\", opts.namespace.trim());\n  if (opts.alt?.trim()) form.set(\"alt\", opts.alt.trim());\n  if (opts.title?.trim()) form.set(\"title\", opts.title.trim());\n\n  const response = await fetch(buildApiUrl(apiBase, path), {\n    method: \"POST\",\n    headers: apiKey ? { authorization: `Bearer ${apiKey}` } : undefined,\n    body: form,\n  });\n  return parseFetchResponse(response);\n}\n\nasync function downloadAsset(apiBase: string, apiKey: string | undefined, assetId: string): Promise<Buffer> {\n  const response = await fetch(buildApiUrl(apiBase, apiPath`/api/assets/${assetId}/content`), {","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/cli/src/commands/client/asset.ts#L77-L113","documentation":"HTTP 404 with body {\"error\":\"Provider vault not found\"} from POST /api/secret-provider-configs/:id/default, second guard (secrets.ts:522). getAccessibleResource already proved the config existed and was company-accessible; svc.setDefaultProviderConfig(id) returning null means the config vanished before the default flag could be set - a concurrent delete (or removal via another mutation) raced this request. Board-only route.","triggerScenarios":"Marking a vault as default at the same moment another board user deletes it; a cleanup script removing test vaults while an operator promotes one to default; two admins switching defaults across overlapping vault sets.","commonSituations":"Environment teardown racing configuration changes; onboarding flows that create a vault, set it default, and delete the previous default while other clients still reference it.","solutions":["On 404, re-fetch GET /api/secret-provider-configs/:id - if also 404, the vault was deleted; choose a different vault to promote.","Coordinate destructive and default-promotion operations (do them in one place, e.g. one admin or one pipeline).","After any default change, re-read the provider config list to learn the current default instead of assuming your POST succeeded.","Retry the promotion with a surviving vault ID if the deleted one was replaced."],"exampleFix":"// before\nawait api.post(`/api/secret-provider-configs/${id}/default`, {});\n\n// after\nlet res = await api.post(`/api/secret-provider-configs/${id}/default`, {});\nif (res.status === 404) {\n  const vaults = await api.listProviderConfigs();\n  const candidate = vaults.find((v) => v.status === 'active');\n  if (!candidate) throw new Error('no active vault to promote');\n  res = await api.post(`/api/secret-provider-configs/${candidate.id}/default`, {});\n}","handlingStrategy":"validation","validationCode":"async function promoteDefaultSafe(api: ApiClient, id: string) {\n  const existing = await api.fetch(`/api/secret-provider-configs/${id}`);\n  if (existing.status === 404) throw new Error(`vault ${id} missing`);\n  const res = await api.fetch(`/api/secret-provider-configs/${id}/default`, { method: 'POST' });\n  if (res.status === 404) {\n    // concurrent delete raced us: pick another active vault\n    const list = await api.fetch('/api/secret-provider-configs').then((r) => r.json());\n    const fallback = (Array.isArray(list) ? list : list.items ?? []).find(\n      (v: { id: string; status: string }) => v.id !== id && v.status === 'active',\n    );\n    if (!fallback) throw new Error('no surviving vault to promote');\n    return api.fetch(`/api/secret-provider-configs/${fallback.id}/default`, { method: 'POST' });\n  }\n  return res;\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}","tryCatchPattern":"try {\n  await api.post(`/api/secret-provider-configs/${id}/default`, {});\n} catch (err) {\n  if (err instanceof ApiError && err.status === 404 && err.body?.error === 'Provider vault not found') {\n    throw new VaultVanishedError(id); // re-select default from fresh list\n  }\n  throw err;\n}","preventionTips":["Centralize default-vault promotion and vault deletion in one workflow so they cannot race.","Re-read the vault list after promotion to confirm which vault is default.","During provider migrations, pause default-promotion jobs until cutover completes.","Prefer promoting a vault you just confirmed via GET in the same second, and handle 404 as 'choose again'."],"tags":["http-404","express","secrets","provider-config","default","race-condition","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"}