Skip to content

Commit 2c86fe3

Browse files
feat(spec): retire ApiKeySchema — sys_api_key has one declaration, the platform object (#8715, ADR-0049) (#8932)
* feat(spec): retire ApiKeySchema — sys_api_key has one declaration, the platform object (#8715, ADR-0049) Maintainer ruling 2026-08-15 (disposition B: delete). ApiKeySchema documented better-auth's apiKey plugin schema — a plugin this platform does not load — so one table had two declarations and the published one was fiction (PD #10). Zero consumers outside its own unit test, re-measured at base 7901b2d. Route 3 retirement kit (#8586 / PR #8702, #8075 precedents): retired-def entry identity/ApiKey + D3 semantic entry identity-api-key-schema-retired under protocol 18; manifest and authorable-surface/authorable-defaults lines removed via the sanctioned deliberate-deletion path the #2978/#4725 gates prescribe; pin tests on both sides of the package boundary; developer-portal prose corrected (the marketplace-key plan is ruled NOT live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt * fix(spec): regenerate strictness-ledger counts; add adr-0087 marker to the changeset (#8715) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt * docs(spec): drop 'API keys' from the generated identity module description (#8715) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3315d1d commit 2c86fe3

19 files changed

Lines changed: 467 additions & 250 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire `ApiKeySchema` — the identity module no longer publishes a second, fictional declaration of `sys_api_key` (#8715, ADR-0049)
6+
7+
<!-- adr-0087: registered identity-api-key-schema-retired -->
8+
9+
**BREAKING** public-surface removal, landing after the v17.0.0 cut (the
10+
lockstep launch-window convention ships it as `minor`; the migration
11+
prescription is registered under protocol major 18, where `os migrate meta`
12+
users will look — the #8586 precedent).
13+
14+
`ApiKeySchema` (and its `ApiKey` / `ApiKeyParsed` types) documented
15+
better-auth's `apiKey` **plugin** schema — a plugin this platform does not
16+
load: `start` and `lastRefetchAt` name columns that do not exist; `enabled`
17+
inverts the real `revoked` column's polarity; `rateLimitEnabled` /
18+
`rateLimitTimeWindow` / `rateLimitMax` / `remaining` advertise a per-key
19+
rate-limit capability nothing implements; `permissions` and `metadata` have no
20+
columns; `organizationId` is camelCase fiction next to the real snake_case
21+
`active_organization_id`. Zero consumers anywhere in the monorepo outside its
22+
own unit test — one table had two declarations, and the published one was
23+
fiction (maintainer-ruled DELETE, 2026-08-15).
24+
25+
**What breaks:** `import { ApiKeySchema, ApiKey, ApiKeyParsed }` from
26+
`@objectstack/spec` or `@objectstack/spec/identity` is TS2305 after upgrade.
27+
The generated reference page's `ApiKey` section and the 19
28+
`identity/ApiKey:*` authorable-surface keys disappear with the schema.
29+
30+
**What stays:** everything real. The single declaration of `sys_api_key` is
31+
the ObjectSchema in `@objectstack/platform-objects`
32+
(`identity/sys-api-key.object.ts`) — columns `name, prefix, user_id,
33+
active_organization_id, scopes, expires_at, last_used_at, revoked, key, id,
34+
created_at, updated_at`; rows are minted by `POST /api/v1/keys` and verified
35+
by `core/src/security/api-key.ts`, keyed by the `osk_` prefix. Neither ever
36+
read the deleted schema, so runtime behaviour is byte-identical.
37+
`UserSchema` / `AccountSchema` / `VerificationTokenSchema` and the
38+
organization module survive unchanged.
39+
40+
The retirement kit:
41+
42+
- schema deleted in place, with the in-module explanatory block naming the
43+
live declaration (`packages/spec/src/identity/identity.zod.ts`)
44+
- ADR-0087 registration: retired-def entry `identity/ApiKey` + D3 semantic
45+
entry `identity-api-key-schema-retired`, both under protocol 18 (route 3 —
46+
no carrier key and no authored document, so no tombstone and no D2
47+
conversion; the registry entries ARE the declaration)
48+
- pin tests: `identity/api-key-retirement.test.ts` (zero holders on every
49+
public entry, survivors stand) and platform-objects'
50+
`sys-api-key-single-declaration.test.ts` (the real column set, spec's
51+
runtime namespace lost the name)
52+
- generated baselines regenerated: authorable surface (−19 keys), JSON-schema
53+
manifest (−1 def), api-surface / export-origins (−3 names), reference docs
54+
- `cloud/developer-portal.zod.ts` prose corrected: marketplace API keys point
55+
at the `sys_api_key` object and `POST /api/v1/keys`, not at
56+
`Identity.ApiKeySchema` (the marketplace-key plan is ruled not live)
57+
58+
## FROM → TO
59+
60+
```ts
61+
// before — type-checked green against a schema no runtime ever read
62+
import { ApiKeySchema, type ApiKey } from '@objectstack/spec/identity';
63+
const key: ApiKey = { id, name, userId, enabled: true, rateLimitMax: 100, /**/ };
64+
65+
// after — read the real table: the sys_api_key ObjectSchema in
66+
// @objectstack/platform-objects (snake_case, `revoked` not `enabled`);
67+
// mint via POST /api/v1/keys, verify via core/src/security/api-key.ts.
68+
import { SysApiKey } from '@objectstack/platform-objects';
69+
```

content/docs/references/cloud/developer-portal.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ Identity module (`@objectstack/spec` Identity namespace), which follows the
2525
better-auth specification. This module only defines marketplace-specific
2626
extensions on top of the shared identity layer:
2727

28-
- **User & Session**`Identity.UserSchema`, `Identity.SessionSchema`
28+
- **User**`Identity.UserSchema`; the session wire shape is `Session` from
29+
`@objectstack/spec/api` (the identity module deliberately declares no bare
30+
`SessionSchema`#4641)
2931
- **Organization & Members**`Identity.OrganizationSchema`, `Identity.MemberSchema`
30-
- **API Keys**`Identity.ApiKeySchema` (with marketplace scopes)
32+
- **API Keys** → the `sys_api_key` ObjectSchema in
33+
`@objectstack/platform-objects` (`identity/sys-api-key.object.ts`), minted via
34+
`POST /api/v1/keys`; its `scopes` column is the marketplace-scope carrier.
35+
There is no `Identity.ApiKeySchema`#8715 retired it (it documented
36+
better-auth's apiKey plugin, which this platform does not load)
3137

3238
## Key Concepts
3339
- **Publisher Profile**: Links an Identity Organization to a marketplace publisher

content/docs/references/identity/identity.mdx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ defines "how to login".
2020
## TypeScript Usage
2121

2222
```typescript
23-
import { AccountSchema, ApiKeySchema, UserSchema, VerificationTokenSchema } from '@objectstack/spec/identity';
24-
import type { Account, ApiKey, User, VerificationToken } from '@objectstack/spec/identity';
23+
import { AccountSchema, UserSchema, VerificationTokenSchema } from '@objectstack/spec/identity';
24+
import type { Account, User, VerificationToken } from '@objectstack/spec/identity';
2525

2626
// Validate data
2727
const result = AccountSchema.parse(data);
@@ -51,35 +51,6 @@ const result = AccountSchema.parse(data);
5151
| **updatedAt** | `string` || Last update timestamp |
5252

5353

54-
---
55-
56-
## ApiKey
57-
58-
### Properties
59-
60-
| Property | Type | Required | Description |
61-
| :--- | :--- | :--- | :--- |
62-
| **id** | `string` || API key identifier |
63-
| **name** | `string` || API key display name |
64-
| **start** | `string` | optional | Key prefix for identification |
65-
| **prefix** | `string` | optional | Custom key prefix |
66-
| **userId** | `string` || Owner user ID |
67-
| **organizationId** | `string` | optional | Scoped organization ID |
68-
| **expiresAt** | `string` | optional | Expiration timestamp |
69-
| **createdAt** | `string` || Creation timestamp |
70-
| **updatedAt** | `string` || Last update timestamp |
71-
| **lastUsedAt** | `string` | optional | Last used timestamp |
72-
| **lastRefetchAt** | `string` | optional | Last refetch timestamp |
73-
| **enabled** | `boolean` || Whether the key is active |
74-
| **rateLimitEnabled** | `boolean` | optional | Whether rate limiting is enabled |
75-
| **rateLimitTimeWindow** | `integer` | optional | Rate limit window (ms) |
76-
| **rateLimitMax** | `integer` | optional | Max requests per window |
77-
| **remaining** | `integer` | optional | Remaining requests |
78-
| **permissions** | `Record<string, boolean>` | optional | Granular permission flags |
79-
| **scopes** | `string[]` | optional | High-level access scopes |
80-
| **metadata** | `Record<string, any>` | optional | Custom metadata |
81-
82-
8354
---
8455

8556
## User

content/docs/references/index.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -24,7 +24,7 @@ counts are sums of the rows they head. Regenerate with
2424
| [Automation Protocol](/docs/references/automation) | 13 | 68 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. |
2626
| [Data Protocol](/docs/references/data) | 29 | 163 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
27-
| [Identity Protocol](/docs/references/identity) | 5 | 28 | Users and accounts, organizations, positions, API keys, SCIM provisioning. |
27+
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
2828
| [Integration Protocol](/docs/references/integration) | 1 | 27 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
2929
| [Kernel Protocol](/docs/references/kernel) | 31 | 176 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
3030
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 36 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 160 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **198** | **1583** | 14 protocol modules |
36+
| **Total** | **198** | **1582** | 14 protocol modules |
3737

3838
---
3939

@@ -186,14 +186,14 @@ Objects, fields, queries, filters, datasources and drivers — the ObjectQL laye
186186

187187
## Identity Protocol
188188

189-
**Source:** `packages/spec/src/identity/` · **Import:** `@objectstack/spec/identity` · **5 pages, 28 schemas**
189+
**Source:** `packages/spec/src/identity/` · **Import:** `@objectstack/spec/identity` · **5 pages, 27 schemas**
190190

191-
Users and accounts, organizations, positions, API keys, SCIM provisioning.
191+
Users and accounts, organizations, positions, SCIM provisioning.
192192

193193
| File | Schemas |
194194
| :--- | :--- |
195195
| [`eval-user.zod.ts`](/docs/references/identity/eval-user) | `EvalUser` |
196-
| [`identity.zod.ts`](/docs/references/identity/identity) | `Account`, `ApiKey`, `User`, `VerificationToken` |
196+
| [`identity.zod.ts`](/docs/references/identity/identity) | `Account`, `User`, `VerificationToken` |
197197
| [`organization.zod.ts`](/docs/references/identity/organization) | `Invitation`, `InvitationStatus`, `Member`, `Organization` |
198198
| [`position.zod.ts`](/docs/references/identity/position) | `Position` |
199199
| [`scim.zod.ts`](/docs/references/identity/scim) | `SCIMAddress`, `SCIMBulkOperation`, `SCIMBulkRequest`, `SCIMBulkResponse`, `SCIMBulkResponseOperation`, `SCIMEmail`, `SCIMEnterpriseUser`, `SCIMError`, `SCIMGroup`, `SCIMGroupReference`, `SCIMListResponse`, `SCIMMemberReference`, `SCIMMeta`, `SCIMName`, `SCIMPatchOperation`, `SCIMPatchRequest`, `SCIMPhoneNumber`, `SCIMUser` |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ directory rather than per file.
261261
| `ai/` | 77 |
262262
| `api/` | 399 |
263263
| `cloud/` | 83 |
264-
| `identity/` | 33 |
264+
| `identity/` | 32 |
265265
| `integration/` | 10 |
266266
| `kernel/` | 296 |
267267
| `qa/` | 6 |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { SysApiKey } from './sys-api-key.object.js';
5+
import * as specIdentity from '@objectstack/spec/identity';
6+
7+
// ─── [#8715] `sys_api_key` has exactly ONE declaration — this object ────────
8+
//
9+
// `@objectstack/spec/identity` used to publish an `ApiKeySchema` that
10+
// documented better-auth's `apiKey` PLUGIN shape — a plugin this platform
11+
// does not load — so the one table had two declarations and the published one
12+
// was fiction (`enabled` vs the real `revoked`, four rate-limit keys with no
13+
// implementation, `start`/`lastRefetchAt`/`permissions`/`metadata` columns
14+
// that do not exist). ADR-0049 enforce-or-remove; maintainer ruling
15+
// 2026-08-15, disposition B: the schema is DELETED and this ObjectSchema is
16+
// the single declaration.
17+
//
18+
// The spec half of the pin (zero holders on every public entry) lives in
19+
// `packages/spec/src/identity/api-key-retirement.test.ts`; this half pins the
20+
// consumer side — spec's runtime namespace really lost the name — and the
21+
// real column set, so a drifted re-declaration cannot come back quietly on
22+
// either side.
23+
describe('[#8715] sys_api_key single-declaration pin', () => {
24+
it('this object declares exactly the real column set', () => {
25+
const declared = Object.keys((SysApiKey as { fields: Record<string, unknown> }).fields).sort();
26+
expect(declared).toEqual([
27+
'active_organization_id',
28+
'created_at',
29+
'expires_at',
30+
'id',
31+
'key',
32+
'last_used_at',
33+
'name',
34+
'prefix',
35+
'revoked',
36+
'scopes',
37+
'updated_at',
38+
'user_id',
39+
]);
40+
// The polarity the deleted schema inverted: the kill switch is `revoked`,
41+
// and there is no `enabled` column (asserted by the exact set above).
42+
expect(declared).toContain('revoked');
43+
});
44+
45+
it('@objectstack/spec/identity no longer exports the fictional schema', () => {
46+
// Value export only, deliberately: `ApiKey` / `ApiKeyParsed` were
47+
// type-only and have no runtime footprint a namespace check could see —
48+
// asserting them here would be vacuous (green before the retirement too).
49+
// The types are covered by the spec-side pin, which reads the built
50+
// export-origins artifact and enumerates types as well as consts.
51+
expect('ApiKeySchema' in specIdentity, 'spec/identity must not export ApiKeySchema (#8715)').toBe(false);
52+
// Anti-vacuity: the namespace import is real and the survivors stand.
53+
expect('UserSchema' in specIdentity).toBe(true);
54+
});
55+
});

packages/spec/api-surface/identity.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
"AUTH_ERROR_CODES (const)",
99
"Account (type)",
1010
"AccountSchema (const)",
11-
"ApiKey (type)",
12-
"ApiKeyParsed (type)",
13-
"ApiKeySchema (const)",
1411
"AuthError (interface)",
1512
"AuthHeaders (interface)",
1613
"AuthResponse (interface)",

packages/spec/authorable-defaults/identity.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \"<def>:<key> = <canonical JSON>\". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.",
33
"category": "identity",
44
"defaults": [
5-
"identity/ApiKey:enabled = true",
65
"identity/EvalUser:positions = []",
76
"identity/Invitation:status = \"pending\"",
87
"identity/Position:delegatable = false",

packages/spec/authorable-surface/identity.json

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,6 @@
1616
"identity/Account:type",
1717
"identity/Account:updatedAt",
1818
"identity/Account:userId",
19-
"identity/ApiKey:createdAt",
20-
"identity/ApiKey:enabled",
21-
"identity/ApiKey:expiresAt",
22-
"identity/ApiKey:id",
23-
"identity/ApiKey:lastRefetchAt",
24-
"identity/ApiKey:lastUsedAt",
25-
"identity/ApiKey:metadata",
26-
"identity/ApiKey:name",
27-
"identity/ApiKey:organizationId",
28-
"identity/ApiKey:permissions",
29-
"identity/ApiKey:prefix",
30-
"identity/ApiKey:rateLimitEnabled",
31-
"identity/ApiKey:rateLimitMax",
32-
"identity/ApiKey:rateLimitTimeWindow",
33-
"identity/ApiKey:remaining",
34-
"identity/ApiKey:scopes",
35-
"identity/ApiKey:start",
36-
"identity/ApiKey:updatedAt",
37-
"identity/ApiKey:userId",
3819
"identity/EvalUser:email",
3920
"identity/EvalUser:id",
4021
"identity/EvalUser:isPlatformAdmin",

packages/spec/export-origins/identity.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
"AUTH_ERROR_CODES": "src/identity/protocol.ts#AUTH_ERROR_CODES (const)",
99
"Account": "src/identity/identity.zod.ts#Account (type)",
1010
"AccountSchema": "src/identity/identity.zod.ts#AccountSchema (const)",
11-
"ApiKey": "src/identity/identity.zod.ts#ApiKey (type)",
12-
"ApiKeyParsed": "src/identity/identity.zod.ts#ApiKeyParsed (type)",
13-
"ApiKeySchema": "src/identity/identity.zod.ts#ApiKeySchema (const)",
1411
"AuthError": "src/identity/protocol.ts#AuthError (interface)",
1512
"AuthHeaders": "src/identity/protocol.ts#AuthHeaders (interface)",
1613
"AuthResponse": "src/identity/protocol.ts#AuthResponse (interface)",

0 commit comments

Comments
 (0)