Skip to content

Commit 3c2b415

Browse files
committed
docs(api,ai): the API-key lifecycle codes reach every face that accepts a key
The runtime dispatcher (MCP HTTP among its routes), the settings routes and the storage download routes now relay the refusal, so the changeset, the error catalog and the MCP connection guide list every face instead of naming three as answering the generic 401. Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
1 parent 469acd6 commit 3c2b415

3 files changed

Lines changed: 8 additions & 9 deletions

File tree

‎.changeset/23367-api-key-lifecycle.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Ruling A on #23367: the key store stays `sys_api_key`, and the policy is auth co
2828
- **The key-admission seam (`resolveApiKeyAdmission`, `@objectstack/core`).** A real key its own state refuses is a new outcome, `lifecycle`, with one of three distinct codes: `API_KEY_REVOKED`, `API_KEY_EXPIRED` (past `expires_at`, or past the grace of a key created with none) or `API_KEY_INACTIVE` (no recorded use for longer than the window). `resolveAuthzContext` raises it as the branded `ApiKeyLifecycleRefusalError` (`403`, a located message naming the key's prefix, the date and the remedy). An unknown key is still `none`.
2929
- **`last_used_at` is written.** Its description promised "Automatically updated on each API call", and nothing wrote it. The seam now stamps it on an admitted (or held) call, at most once an hour per key, and the inactivity judgment reads the value as it was BEFORE that call's stamp, so a key's own use cannot revive it. A stamp that fails never fails the request; it is said once, at `error`.
3030
- **Keys created before the policy.** A key with no expiry gets one year from `api_key_policy_started_at`; the date is stamped onto its `expires_at` the first time it is admitted, so the keys page shows it, and the refusal after it names it. A key with no recorded use counts its inactivity from the same anchor (or from its own creation, when later) — never from an earlier `created_at`, since no use was ever recorded before the policy.
31-
- **Every face that admits keys** passes the policy source (`apiKeyLifecyclePolicySourceOf(authService)`, a required input beside `userAuthGates`). The REST data API, the datasource admin routes, the share-link routes, the marketplace install-local routes and the MCP stdio transport answer the refusal's `403` and code. The runtime dispatcher (the MCP HTTP endpoint among its routes), the settings routes and the storage routes refuse the same key with their generic `401` — fail-closed, never an admission — and the server log names the code.
31+
- **Every face that admits keys** passes the policy source (`apiKeyLifecyclePolicySourceOf(authService)`, a required input beside `userAuthGates`). Every one of them answers the refusal's `403` and code: the runtime dispatcher (the MCP HTTP endpoint among its routes), the REST data API, the settings routes, the storage download routes, the datasource admin routes, the share-link routes, the marketplace install-local routes and the MCP stdio transport. Each relays the refusal by its brand, so every other identity fault at those doors keeps its existing answer.
3232
- **`OS_CLOUD_API_KEY` is untouched.** It is the runtime's outbound credential to the control plane, never admitted by this seam.
3333

3434
What changes for you:
@@ -37,5 +37,5 @@ What changes for you:
3737
- **A signing asking for more than a year is refused** `400 API_KEY_EXPIRY_TOO_LONG`, where it used to be accepted. Ask for the date the message names, or an earlier one.
3838
- **A key unused for 90 days is refused** `403 API_KEY_INACTIVE`, where it used to be served. It is not revoked; the remedy is a new key.
3939
- **A key that already existed with no expiry keeps working for one year** from the date the policy took effect on your deployment (`auth.api_key_policy_started_at`), then answers `403 API_KEY_EXPIRED` naming that date. Replace such keys within the year.
40-
- **A revoked or expired key answers its own code** (`403 API_KEY_REVOKED` / `API_KEY_EXPIRED`) on the faces listed above, where it used to answer the generic `401`; and a session cookie sent beside such a key no longer answers for it.
40+
- **A revoked or expired key answers its own code** (`403 API_KEY_REVOKED` / `API_KEY_EXPIRED`) on every face that accepts a key, where it used to answer the generic `401`; and a session cookie sent beside such a key no longer answers for it.
4141
- **Hosts calling `resolveAuthzContext`, `resolveApiKeyAdmission` or `resolveApiKeyPrincipal` directly** pass `apiKeyLifecycle: apiKeyLifecyclePolicySourceOf(authService)` (or the sixth argument), or an explicit `undefined` where no auth service exists. A caller of `resolveAuthzContext` that wants the located answer re-raises `ApiKeyLifecycleRefusalError` (`isApiKeyLifecycleRefusalError`) the way it re-raises the store outage. Code switching exhaustively over `ApiKeyAdmission['outcome']` handles the new `lifecycle` outcome.

‎content/docs/ai/connect-mcp.mdx‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,11 @@ create the new key first, switch the integration over, then revoke the old one.
168168
An unknown key (a typo, a key that never existed) still gets the generic
169169
`401 UNAUTHENTICATED`.
170170

171-
These codes reach the caller on the REST data API (`/api/v1/data/…`), the
172-
datasource admin and share-link routes, the marketplace install routes and the
173-
MCP **stdio** transport. The other endpoints the runtime serves — the MCP HTTP
174-
endpoint (`/api/v1/mcp`) among them — and the settings and file-storage routes
175-
refuse the same key with the generic `401 UNAUTHENTICATED`; the server log
176-
names the code there.
171+
These codes reach the caller on every endpoint that accepts a key: the MCP
172+
HTTP endpoint (`/api/v1/mcp`) and the other endpoints the runtime serves, the
173+
REST data API (`/api/v1/data/…`), the settings, file-download, datasource
174+
admin and share-link routes, the marketplace install routes, and the MCP
175+
**stdio** transport.
177176

178177
Send it on every request in any of three equivalent forms:
179178

‎content/docs/api/error-catalog.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ result set — a response indistinguishable from a successful query.
449449
**Fix:** Create a new key and replace this one wherever it is configured. A revoked key never works again.
450450
**Retry:** `no_retry`
451451

452-
These three are answered by the REST data API, the datasource admin and share-link routes, the marketplace install routes and the MCP stdio transport. The other endpoints the runtime serves (the MCP HTTP endpoint among them) and the settings and file-storage routes refuse the same key with the generic `401 UNAUTHENTICATED`. An unknown key is always the generic 401.
452+
These three are answered on every endpoint that accepts an API key: the endpoints the runtime serves (the MCP HTTP endpoint among them), the REST data API, the settings, file-download, datasource admin and share-link routes, the marketplace install routes and the MCP stdio transport. An unknown key is always the generic 401.
453453

454454
---
455455

0 commit comments

Comments
 (0)