refactor(auth)!: stop advertising passkeys/magicLink on /api/v1/auth/config (#7481) - #7508
Conversation
…config (#7481) Both flags were served by `getPublicConfig()` from introduction and read by no client: no login UI anywhere renders a passkey or magic-link affordance off them. The payload therefore advertised two sign-in methods a user could never reach, and a deployer who set either plugin flag flipped a switch with no observable effect anywhere — ADR-0049 enforce-or-remove, on a deployment-facing contract. Maintainer ruling 2026-08-11 chose stop-advertising over keep-as-reserved: declared = enforced. The two capabilities are not equally empty, and the prescriptions say so separately rather than sharing one string. Nothing is wired behind `passkeys`. `magicLink` loses only its ADVERTISEMENT: `plugins.magicLink` still wires better-auth's magic-link plugin and `/magic-link/{send,verify}` answer as before. - `AuthFeaturesConfigSchema`: both keys tombstoned with `retiredKey()` rather than deleted — the schema is not `.strict()`, so a plain delete would let a payload carrying either key parse clean and lose it in silence (ADR-0104). - ADR-0087: one D3 `SemanticMigration` plus the two exact `RETIRED_KEYS_BY_MAJOR` entries. No D2 conversion, deliberately — a response surface has no authored source for `os migrate meta` to rewrite (the `EnhancedApiError.fieldErrors` disposition). - `PUBLIC_AUTH_FEATURES`: the two entries are removed, which is what the drift guard requires once the served set loses them, and it also narrows `requiresFeature` so no spec input can gate on an unserved flag. Their standing record moves to the new `PUBLIC_AUTH_FEATURES_NOT_ADVERTISED`, re-anchored off closed objectui#2514 onto objectui#4179. - Generated baselines, spec-changes, upgrade guide, export-origins and reference docs regenerated; hand-written auth/SSO docs and the identity-auth QA checklist corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M33yPsTgKmkB5rh2WzZwQk
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #7481
Implements the maintainer ruling recorded on the issue on 2026-08-11, quoted verbatim and untranslated:
What changed
Both flags were served by
getPublicConfig()from introduction and read by no client — no login UI anywhere renders a passkey or magic-link affordance off them. The payload therefore advertised two sign-in methods a user could never reach, and a deployer who set either plugin flag flipped a switch with no observable effect anywhere.The two capabilities are not equally empty, and the prescriptions say so separately rather than sharing one string:
passkeys— nothing is wired behind it at all; no better-auth passkey plugin is registered, so/passkey/*does not answer. There is no capability to detect.magicLink— loses only its advertisement.AuthPluginConfig.plugins.magicLinkstill wires better-auth's magic-link plugin, and/api/v1/auth/magic-link/send+/magic-link/verifyanswer exactly as before. A custom UI can drive them today.The server-side authoring surface (
AuthPluginConfig.plugins.*) is untouched — this narrows the served payload, not the deployment configuration.The retirement kit
AuthFeaturesConfigSchemais not.strict(), so a plain delete would let a payload carrying either key parse clean and lose it in silence (the ADR-0104 shape). Measured, not assumed: plain removal trippedcheck:authorable-surfacegate (a),2 authorable key(s) disappeared from the contract.retiredKey()) on both keys, each with its own prescription.SemanticMigration(auth-config-unadvertised-reserved-features) plus the two exactRETIRED_KEYS_BY_MAJORentries. No D2 conversion, deliberately — this is a response surface the server mints per request, so there is no authored source foros migrate metato rewrite. That is theEnhancedApiError.fieldErrors/api/ListNotificationsRequest:cursordisposition, and the prescriptions correspondingly carry noos migrate metasentence.spec-changes.json, upgrade guide,export-origins/and reference docs regenerated. Hand-written auth/SSO docs and theidentity-authQA checklist corrected — the checklist's flag-advertisement clause now inverts: seeing either key in the payload is itself a FAIL.Registry shape — disclosed, not silently invented
The ruling presumes the two
exempt.reasonentries survive to be re-anchored. They cannot: the drift guard asserts the served key set is equivalent toPUBLIC_AUTH_FEATURES, so once the payload loses them the registry must too — and an entry is also what makes a flagrequiresFeature-gateable, i.e. keeping them would leave spec inputs able to gate on a capability that is never served.So the entries are removed and the honest record moves to a new exported constant in the same file,
PUBLIC_AUTH_FEATURES_NOT_ADVERTISED, pointing at objectui#4179. This is new registry shape, added openly: it parallels the existingPUBLIC_AUTH_CONFIG_NON_FLAG_KEYS(a list of keys deliberately not classified as flags), and it is pinned as a negative — these names must be absent from the registry, absent from the payload, and un-gateable. The ruling's intent is met: no live pointer at closed objectui#2514 remains anywhere. One historical mention survives inside the new docblock, which names objectui#2514 explicitly as closed while recording why the flags were reserved; that is provenance, not a stale pointer.Verification
Build closure first in a fresh worktree, then:
@objectstack/spec test@objectstack/plugin-auth test@objectstack/dogfood testspec+plugin-authtypecheckcheck:generatedAll 13 generated artifacts are up to datecheck:liveness·check:empty-state·check:authorable-surface·check:docs·check:api-surface·check:spec-changes·check:upgrade-guide·check:migration-registry·check:skill-refs·check:skill-docs·check:skill-examples·check:variant-docs·check:export-originscheck:adr-anchors·check:changeset-gate-self-tests·check:docs-audit-scope·check:i18n·check:merge-driver·check:release-body·check:spec-parsed-alias·check:nul-bytesDownstream consumer sweep — prefix direction (
--filter '...'is downstream; here run per-package after building each one's full dependency closure):@objectstack/client,@objectstack/hono,@objectstack/dogfood,@objectstack/platform-objects,@objectstack/lint,@objectstack/cliall typecheck green. An earlier red onhonoandclientwas the stale-artefact trap (unbuilt siblings), not this change; it cleared once the closures were built.Reverse verification — direction predicted before running: re-serving
passkeysin thefeaturesliteral should turn the drift guard and the new pin red. It did, three ways at once:restored → 997/997 green again.
Two notes for the reviewer
check:doc-formula-expressions, named in the dispatch card's gate list, does not exist in this repo (Command "check:doc-formula-expressions" not found). The nearest real gate,check:doc-authoring, was run instead and passes.merge=os-regen, but the branch is based onorigin/mainwith no merge performed, so AGENTS.md's four-step regen-on-merge sequence has no merge to apply to.check:generatedis green.Out of scope, filed separately:
buildPluginList()'s localenabled.passkeysis dead — nothing reads it, and no passkey plugin is constructed from it. Pre-existing and untouched here.Generated by Claude Code