Skip to content

Consider a registered error code for an API key refused by tenancy posture — today it answers the standard UNAUTHENTICATED #8708

Description

@qq9340100

Filed from #8287 (PR: claude/issue-8287-api-key-organization). Routed to the packages/spec seat because the only file that can resolve it is packages/spec/src/api/error-code-ledger.zod.ts.

Context

#8287 made API keys carry an organization. Two refusals now exist at verify time:

  • an org-less key under isolated — provably dead there (the wall is organization_id = activeOrganizationId), so it is refused rather than allowed to answer 200 + total 0;
  • a key whose owner's membership in its organization has ended.

Its follow-through decision asked for these to be refused "with a distinguishable error code".

What shipped instead, and why

Both refusals answer the standard catalog member UNAUTHENTICATED (401). The behavioural requirement is met — the failure is loud at call time instead of a silent empty result — but the two refusals are not distinguishable on the wire from any other unauthenticated request.

Two reasons this was not resolved in that PR, the second of which may mean there is nothing to do here:

  1. ERROR_CODE_LEDGER lives in packages/spec, which that card placed absolutely off-limits to the implementing seat.
  2. The ledger's own admission rule may forbid it anyway: "If the condition is generic (not found / permission / validation / rate limit), use the standard catalog instead of registering a synonym" — and since [finding] @objectstack/rest registers four generic synonyms the standard catalog already covers (CONFLICT, NOT_FOUND, FORBIDDEN, INTERNAL) — contract call, not a cleanup #8211 that is mechanical, with standardSynonymOf refusing a new code that shadows a standard member unless a STANDARD_SYNONYM_WAIVERS entry records why. A refused credential answering 401 is a strong candidate for exactly that refusal.

A machine-readable discriminator does exist internally: ResolvedAuthzContext.authRefusal.reason ('organization_required' / 'organization_membership_ended'), deliberately lowercase so it cannot be mistaken for a wire code. Nothing currently surfaces it on the wire.

The decision this needs

Either:

  • (A) leave itUNAUTHENTICATED is the honest standard member; the operator-facing detail rides in the message. Then this issue closes as "working as the ledger intends", and the value is having the question on the record.
  • (B) register API_KEY_ORGANIZATION_REQUIRED (and possibly a membership sibling) with a STANDARD_SYNONYM_WAIVERS entry recording what it shadows and why — justified by the operator-debugging case: an automation that breaks after a posture change or a membership removal currently reports the same 401 as a missing header.

Worth noting for (B): the deployments most affected are running isolated with existing org-less keys, where every such key starts refusing at once — a distinguishable code is exactly what turns that from a mystery into a one-line diagnosis.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions