Filed by the dev on #8026 as an out-of-scope finding of that card. Unassigned, no pm:queue — for triage to grade.
Blocked-by: #8026 (the error class this is about is introduced there).
The gap
#8026 makes the settings write path fail closed: a declared-encrypted value (encrypted: true / manifest type: 'password') is refused with SettingsCryptoUnavailableError when nothing able to encrypt it is wired. The class carries code = 'SETTINGS_CRYPTO_UNAVAILABLE', and that code is in-process only.
settings-routes.ts does not map it, so over REST the refusal takes the same 500 / INTERNAL_ERROR arm every unmapped service error takes. The full actionable message is carried, so an operator reading logs is fine — but a client (the Setup UI is the consumer) cannot distinguish "the deployment cannot encrypt secrets, reconfigure it" from "the server crashed", and so cannot render the one thing an admin needs to see.
Every other settings error class has a registered wire code and a dedicated status: SETTINGS_LOCKED 409, SETTINGS_UNKNOWN_KEY 400, SETTINGS_VALIDATION 400, SETTINGS_FORBIDDEN 403, UNKNOWN_NAMESPACE 404. This one is the odd one out.
Why it was left out of #8026
Not an oversight — a scope boundary. A dedicated wire code must first be registered in ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts), because ApiErrorSchema.code validates against the closed StandardErrorCode union the ledger, and emitting an unregistered code is precisely the silent fourth state ADR-0112 forbids. #8026's dispatch excluded packages/spec/src/**, so the honest in-scope choice was to leave the refusal on the generic arm rather than put an unregistered code on the wire.
What the fix looks like
- Register
SETTINGS_CRYPTO_UNAVAILABLE in ERROR_CODE_LEDGER under @objectstack/service-settings, next to the other SETTINGS_* rows. - Map it in
settings-routes.ts's PUT handler. - Decide the status. Worth an actual decision rather than a default: 500 says "server fault", which is true and matches today's behaviour; 503 says "temporarily unavailable", which invites a client retry that will fail forever until an operator wires a provider. My weak preference is 500 with the specific code (the code carries the meaning, the status stays honest), but this is exactly the kind of call that should be made once, in the open.
- Pin it: a refusal test asserting both
code and status on the envelope already exists in settings-crypto-fail-closed.test.ts and would be re-pointed.
Severity
Low. Nothing is unsafe; the refusal already holds and its message already carries the fix. This is about whether a client can act on it programmatically.
Filed by the dev on #8026 as an out-of-scope finding of that card. Unassigned, no
pm:queue— for triage to grade.Blocked-by: #8026 (the error class this is about is introduced there).
The gap
#8026 makes the settings write path fail closed: a declared-encrypted value (
encrypted: true/ manifesttype: 'password') is refused withSettingsCryptoUnavailableErrorwhen nothing able to encrypt it is wired. The class carriescode = 'SETTINGS_CRYPTO_UNAVAILABLE', and that code is in-process only.settings-routes.tsdoes not map it, so over REST the refusal takes the same500/INTERNAL_ERRORarm every unmapped service error takes. The full actionable message is carried, so an operator reading logs is fine — but a client (the Setup UI is the consumer) cannot distinguish "the deployment cannot encrypt secrets, reconfigure it" from "the server crashed", and so cannot render the one thing an admin needs to see.Every other settings error class has a registered wire code and a dedicated status:
SETTINGS_LOCKED409,SETTINGS_UNKNOWN_KEY400,SETTINGS_VALIDATION400,SETTINGS_FORBIDDEN403,UNKNOWN_NAMESPACE404. This one is the odd one out.Why it was left out of #8026
Not an oversight — a scope boundary. A dedicated wire code must first be registered in
ERROR_CODE_LEDGER(packages/spec/src/api/error-code-ledger.zod.ts), becauseApiErrorSchema.codevalidates against the closedStandardErrorCodeunion the ledger, and emitting an unregistered code is precisely the silent fourth state ADR-0112 forbids. #8026's dispatch excludedpackages/spec/src/**, so the honest in-scope choice was to leave the refusal on the generic arm rather than put an unregistered code on the wire.What the fix looks like
SETTINGS_CRYPTO_UNAVAILABLEinERROR_CODE_LEDGERunder@objectstack/service-settings, next to the otherSETTINGS_*rows.settings-routes.ts's PUT handler.codeandstatuson the envelope already exists insettings-crypto-fail-closed.test.tsand would be re-pointed.Severity
Low. Nothing is unsafe; the refusal already holds and its message already carries the fix. This is about whether a client can act on it programmatically.