{"record":{"id":"b28d027d43cfdcb7","repo":"actualbudget/actual","slug":"invalid-secret-name","errorCode":"invalid-secret-name","errorMessage":"Unknown secret name","messagePattern":"Unknown secret name","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/sync-server/src/app-secrets.js","lineNumber":42,"sourceCode":"// Per-budget-file secrets are managed by file owners\nfunction isBudgetFileOwner(fileId, userId) {\n  const { granted } = UserService.checkFilePermission(fileId, userId) || {\n    granted: 0,\n  };\n  return granted > 0;\n}\n\nfunction canManagePerBudgetFileSecrets(fileId, userId) {\n  return isAdmin(userId) || isBudgetFileOwner(fileId, userId);\n}\n\napp.post('/', async (req, res) => {\n  const { name, value } = req.body || {};\n  const fileId = req.get('X-Actual-File-Id');\n  const perBudgetFile = fileId != null;\n\n  if (!(name in SecretName)) {\n    res.status(400).send({\n      status: 'error',\n      reason: 'invalid-secret-name',\n      details: 'Unknown secret name',\n    });\n    return;\n  }\n\n  if (perBudgetFile) {\n    if (!isValidFileId(fileId)) {\n      res.status(400).send({\n        status: 'error',\n        reason: 'invalid-file-id',\n        details: 'invalid fileId',\n      });\n      return;\n    }\n\n    if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-secrets.js#L24-L60","documentation":"The POST /secrets endpoint validates that the requested secret name is one of the known entries in the SecretName enum before storing it. If the body's 'name' is absent, misspelled, or not part of the supported set, the server rejects the request with HTTP 400 and reason 'invalid-secret-name'. This prevents arbitrary keys from being written into the secret store.","triggerScenarios":"POST /secrets with a body whose 'name' is not a key of SecretName (e.g. typo, unknown provider name, name field missing entirely so 'undefined in SecretName' is false); API client using an outdated secret name after a rename in the SecretName enum.","commonSituations":"Hand-rolled scripts or integrations posting a secret name guessed from documentation of a different Actual version; provider credentials (e.g. gocardless/pluggy key names) changed between server versions; JSON body missing 'name' because of a content-type/parse issue.","solutions":["Use an exact SecretName value from packages/sync-server/src/services/secrets-service.js (e.g. the gocardless/pluggy credential names) as the 'name' field.","Print or fetch the current SecretName enum from the server source version you run and update your integration to match.","Ensure the request body actually contains 'name' and Content-Type is application/json so express.json() parses it.","Upgrade or align client and server versions so both agree on the secret name set."],"exampleFix":"// before\nawait fetch('/secrets', { method: 'POST', body: JSON.stringify({ name: 'pluggy_key', value }) });\n// after\nawait fetch('/secrets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'pluggyai_client_secret', value }) });","handlingStrategy":"validation","validationCode":"const KNOWN_SECRET_NAMES = ['gocardless_secret_id','gocardless_account_id','pluggyai_client_id','pluggyai_client_secret'];\nfunction isValidSecretName(name) {\n  return typeof name === 'string' && KNOWN_SECRET_NAMES.includes(name);\n}","typeGuard":"function isSecretName(name) {\n  return typeof name === 'string' && name in SecretName;\n}","tryCatchPattern":"const res = await fetch('/secrets', { method: 'POST', body: JSON.stringify({ name, value }) });\nif (res.status === 400) {\n  const body = await res.json();\n  if (body.reason === 'invalid-secret-name') throw new Error(`Unknown secret name: ${name}`);\n}","preventionTips":["Source secret names from the SecretName enum of the deployed server version, not from memory","Keep client and server versions in sync","Always send Content-Type: application/json so 'name' is parsed","Lint/check integration constants against the enum when upgrading"],"tags":["http-400","validation","secrets","api"],"backgroundTag":"invalid-enum-value","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}