Skip to content

managed-extension-fields D7 collision guard silently skips sys_api_key (no apikey entry in MODEL_TO_OBJECT) #7770

Description

@claude

Observation

managed-extension-fields.test.ts (the ADR-0105 D7 collision guard) derives better-auth's real field surface from getAuthTables() and fails on any overlap with MANAGED_EXTENSION_FIELDS. Its MODEL_TO_OBJECT map carries nine models — user, session, account, verification, organization, member, invitation, team, teamMember — and no apikey entry. Any object absent from that map is skipped outright:

const owned = byObject[object];
if (!owned) continue;

sys_api_key declares managedBy: 'better-auth', so it looks like it is covered. It is not — its extension fields get zero collision coverage.

Why this is dormant today, not a live defect

better-auth's apiKey plugin is not loaded (auth-manager.ts assembles bearer + the optional set; apiKey is not among them). sys_api_key is hand-rolled: packages/core/src/security/api-key.ts mints and verifies it, and POST /api/v1/keys writes the row. So better-auth owns none of these columns, there is nothing to collide with, and the guard skipping the object is currently the correct answer.

Adding apikey: 'sys_api_key' to MODEL_TO_OBJECT today would change nothing either, since getAuthTables() is called without that plugin and would not emit an apikey model.

The latent part

If better-auth's apiKey plugin is ever enabled, its own model carries name, prefix, key, userId, enabled, expiresAt, permissions, metadata and the refill/rate-limit columns — several of which are exactly the columns sys_api_key already defines and writes. That is the ownership-transfer hazard D7 exists to catch, and the gate that is supposed to catch it would stay silent, because the object is not in its map.

Worth noting the shape: D7's guard is not wrong, it is unreachable for this object. That is the same class as objectstack#7727 (a declared row action behind a method gate that answered 405 first) — a check that reads as coverage and evaluates to nothing.

Suggested direction (not decided here)

Whoever enables better-auth's apiKey plugin must, in the same change, add apikey: 'sys_api_key' to MODEL_TO_OBJECT, pass the plugin into that test's getAuthTables() call, and reconcile the overlapping columns — most likely by keeping the hand-rolled table and NOT adopting the plugin's model, or by renaming ours. A cheaper interim option is a comment in MODEL_TO_OBJECT recording why apikey is deliberately absent, so its absence reads as a decision rather than an oversight.

Filing unassigned and finding-labeled: nothing a user hits today, and the trigger is a plugin-enablement decision nobody has taken.

Source

Found while implementing #7727 (PR #7769), which registers sys_api_key → {revoked} in MANAGED_EXTENSION_FIELDS / MANAGED_EXTENSION_EDITABLE_FIELDS. revoked is not among better-auth's apikey columns, so that entry is safe either way — but it is the first entry this object has ever had in that registry, which is what surfaced the gap.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions