Repository navigation
plugin-auth: decide the durable answer to better-auth's account-issuer rollback — 1.7.3 removed the issuer identity outright, and #16186 shipped an exact pin as a stopgap #16629
Description
Activity
Four-facet block added by the director seat (summon #17,
session_01XesLUWmuhjuRwmU618AZ1M, 2026-09-07T14:4xZ) so the card can be presented in a decision batch; the card arrived in the decision box with no analysis. ⛔ Not a ruling. Facts below are read from the card body and frompackages/plugins/plugin-authonorigin/main; the pinned line is the #16186 stopgap.四棱分析(总监席出具,供裁决)
- ① 项目长远合理性(≥50%,领起推荐)—— 指向 1(采纳上游回滚)。
sys_account.issuer、启动期回填、issuer 派生对齐测试,这三样存在的唯一理由是「服务 better-auth 1.7 的账户身份模型」;上游在 1.7.3 把这个模型整个撤走了(better-auth#10909),不是改名。选 2 等于把一个供应商已放弃的身份模型变成我们自己永久拥有的分叉——认证库上的自有分叉是本仓最不该长期持有的东西;选 3 把「暂时钉死」默认退化成「永远钉死」。1 是三条里唯一缩小特例的:列、回填、对齐测试整体退役,findAccountByKey回到(providerId, accountId)。 - ② 实际业务拉动 —— 真实但不是客户报障。 拉动来自安全姿态:
plugin-auth是认证库,精确钉在1.7.2意味着上游任何安全补丁都要先做一次「受审的解钉」才能进来,且差距逐版扩大;check:vendor-export-contract已保证解钉不会误发生,但也保证了它不会自动发生。⛔ 没有任何客户报过这件事;拉动是「不做的代价随时间增长」,不是「今天有人被挡」。 - ③ 防 AI 犯错 —— 指向 1,且反对 2。 3 是静默腐烂的标准形状:一个没人会再看的精确钉;1 的每一步都响亮——迁移是显式的,门禁对任何掉符号的解钉拒绝;2 最差:自有一套账户解析逻辑是一个安全敏感的新面,AI 以后每次动 auth 都要同时维持它与供应商的差异,而这个差异没有上游测试为它背书。
- ④ 创业阶段不扩散 —— 1 净减代码,2 净增永久义务,3 现在零、随时间增。
⚠️ 1 的代价是一次身份表迁移:删列、删(issuer, account_id)唯一索引、现有行按(providerId, accountId)重新判碰撞。创业阶段部署少、行少,这笔成本今天最低、以后只涨——这是本卡不该等的理由,不是可以渐进的理由(2026-08-27 裁定:短期不考虑渐进)。
推荐:1。回退:3,但必须带到期条件(写明「到上游哪个版本 / 哪个日期前必须解钉」,否则 3 就是 2 的沉默版)。⛔ 2 不可荐——认证库上的自有分叉。
置信缺口(裁 1 的派发前提,⛔ 不是裁决的前提):现有数据库里
(issuer, account_id)唯一而(providerId, accountId)会碰撞的行数——按供应商逐一数(Entra 的 per-login issuer 解析是最可能碰撞的那一家)。零 ⇒ 迁移是纯删列;非零 ⇒ 迁移必须拒绝并列出碰撞行,⛔ 不得静默合并或静默丢弃任何一行。裁后执行(裁 1):
needs-user-decision→pm:queue,domain:services席派发;Clause-②: yes(sys_account平台对象删一个已声明字段、@objectstack/platform-objects已发布.d.ts收窄)⇒ 契约档或载体补偿;changeset major 分肢按 ADR-0087 带迁移台账行(删列 + 索引换键 + 碰撞处置);同 PR 抬@better-auth/*家族到1.7.3一条线、退役backfill-account-issuer.ts及其测试与account-issuer-parity.test.ts、四份翻译包去掉该标签。派发令第一步 = 上面那个碰撞数,贴在卡上再动 schema。维护者速读
我们的登录库(better-auth)上个月把「账户属于哪个签发方」这套模型整个撤掉了,不是改名。我们为了适配它 1.7 版本,曾经给账户表加了一列、写了启动时的回填、还写了对齐测试。现在上游没有这套东西了,#16186 只能先把版本精确钉死在旧版,保证新装能用。
要你拍板的:1(推荐)跟着上游走——删掉那一列和回填,做一次账户表迁移,版本解钉;2 把那套身份模型当成我们自己的、永远维护;3 一直钉着旧版——安全补丁进不来。
风险与代价:1 要做一次身份表迁移,现在用户少、成本最低;派发时先数一遍现有数据有没有会撞在一起的账户行,有则迁移必须拒绝并列出,不会静默合并。2 是在登录库上养一个自己的分叉。3 是把问题往后拖、越拖越贵。
你要做的:回一个数字 —— 1 / 2 / 3。
Generated by Claude Code
- ① 项目长远合理性(≥50%,领起推荐)—— 指向 1(采纳上游回滚)。
Ruling recorded — 1: adopt better-auth's account-issuer rollback —
sys_account.issuerand the backfill retire, the family lifts to 1.7.3, existing rows migrate with collisions refused, never merged (director seat, summon #17, decision batch #3, 2026-09-07)Provenance (who / verbatim / where): maintainer, live PM chat with the director seat (
session_01XesLUWmuhjuRwmU618AZ1M), 2026-09-07T14:5xZ, batch #3 presented as1(1) · 2(1) · 3A · 4C · 5Awith this card as item 2 recommending 1 (the 5572249043 four-facet block, four facets aligned); reply, verbatim: 「同意」.Ruled. The platform tracks the vendor's identity model; it does not own a fork of one. Everything that existed only to serve better-auth's 1.7 issuer model retires: the
issuerfield onpackages/platform-objects/src/identity/sys-account.object.tsand its label in the four translation bundles,packages/plugins/plugin-auth/src/backfill-account-issuer.tswith its test,account-issuer-parity.test.ts, and the column mappings inauth-schema-config.ts/managed-extension-fields.ts/auth-manager.ts.findAccountByKeykeys on(providerId, accountId)again. The@better-auth/*family moves as one line to1.7.3, andcheck:vendor-export-contractis green on the lifted line. Option 2 (keep the 1.7 model as ours) is refused — a permanent fork on the authentication library. Option 3 (stay pinned) is refused as the durable answer: it was the #16186 stopgap and it has done its job.Measurement before any schema moves (the dev's first step, posted on this card): per provider, the count of existing rows that are unique under
(issuer, account_id)and collide under(providerId, accountId)— Entra's per-login issuer resolver is the likeliest source. Zero ⇒ the migration is a column drop and an index re-key. Non-zero ⇒ the migration refuses and lists the colliding rows; ⛔ it never merges or drops a row silently.Execution,
domain:serviceslane: one PR — the platform-object change, the migration for existing deployments, the retirements, the family lift.Clause-②: yes(a platform object drops a declared field; a published.d.tsnarrows) ⇒ dispatch atCONTRACT_REVIEW_TIER, or withneeds:contract-reviewas compensation. Changeset major arm for@objectstack/platform-objectsand@objectstack/plugin-authper ADR-0087, with the migration ledger row (column drop, index re-key, collision handling) and the BREAKING banner. One step, no staged window (2026-08-27 ruling). Priority andtypewere not graded at filing; the services seat grades at claim (the director's reading: p2 — an exact pin on the authentication library blocks security patches).Labels:
needs-user-decision→pm:queuein one write, read back.Blocked-by:none (#16186 landed). Governing text: ADR-0087 (a breaking change carries its migration),check:vendor-export-contract(#16186). Ledger: objectstack#12708, summon #17.
Generated by Claude Code
Maintainer ruling: Option 1 — adopt the rollback
Authority — maintainer instruction, verbatim, in this PM session's chat at 2026-09-09T03:5xZ (⛔ 照抄不译):
但是我们不可能长期保持旧版本不升级,总要找到方案
残留的 sys_account.issuer 列 退役,开卡
⇒ Option 3 (stay pinned) is rejected as a durable answer, and Option 1 is selected: drop
sys_account.issuer, retire the backfill, migrate existing rows, raise the better-auth family floor to1.7.3. Option 2 (own the divergence forever) is not taken.⛔ No new card was opened. The maintainer asked for one; this card already IS it, and its Option 1 already scopes the column retirement. Splitting the platform-objects half out would produce a fragment that cannot land alone — the column, the backfill and the version floor have to move together (this card's own closing note: the family moves as one line,
@better-auth/core@1.7.2and@better-auth/kysely-adapter@1.7.3being mutually incompatible in both directions). Recorded here instead, per 查重先行.Independent corroboration of this card's premise
Re-measured from the published tarballs today, before the ruling was recorded — this card's reading holds:
reading 1.7.2 1.7.3 dist/db/schema/account.mjssize1642 bytes 938 bytes issuerin that schemaissuer: z.string()absent Issuer-shaped exports indist/createLocalAccountIssuer,createOAuthAccountIssuer,accountIssuer,encodeAccountIssuerProviderId,Issuer,setIssuerIssuer,hasIssuer,setIssueronlydb/index.d.mtsIssuer declarations2 exported 0 ⭐ Positive control on the zeros (a zero is only a reading with one): the same matcher found
accountId,providerId,accessToken,refreshToken,scopeanduserIdin both versions' account schema — six fields present on both sides,issuerpresent on one. The file exists in both; it did not vanish, it shrank.⚠️ A successor-hunt was run and came back negative, with the near-miss named so a later reader does not re-open it: 1.7.3 does carryhasIssuer/setIssuer, but they live indist/oauth2/client-assertion.mjsanddist/db/schema-diff.mjs— the OAuth2 client-assertioniss, a different concept from account identity. ⛔ They are not a migration target.Sequencing — this does NOT block the release in flight
⛔
sys_account.issueris load-bearing while the pin is at 1.7.2: better-auth 1.7.2 resolves sign-in throughfindAccountByKey({ issuer, accountId }), so retiring the column before the floor moves would make every existing account unresolvable. The retirement is downstream of the version lift, in the same change, never ahead of it.⇒ The pending
17.4.0release (exact1.7.2, the #16186 stopgap) ships unchanged and unblocked by this ruling. This card is the durable follow-up, not a release blocker.维护者速读
⭐ 你的裁决已记录:选 1 —— 跟着上游回滚走。 不新开卡,因为这张卡本身就是它,而且 Option 1 里已经写了要 drop 掉
sys_account.issuer。⛔ 拆成两张会得到一张落不了地的卡:列、backfill、版本地板必须同一次一起动(1.7.2 和 kysely-adapter 1.7.3 双向不兼容,这一家必须整条线一起挪)。
✅ 我今天独立复测过这张卡的前提,成立:1.7.3 的 account schema 从 1642 字节缩到 938 字节,
issuer整个没了;而accountId/providerId等六个字段两边都在(阳性对照)。也追过"是不是改名了"—— 1.7.3 里那个hasIssuer是 OAuth2 client assertion 的iss,不是同一个概念,别当迁移目标。⚠️ 顺序上有一条硬约束:在 pin 还是 1.7.2 的时候,sys_account.issuer是登录要用的(1.7.2 靠(issuer, accountId)找账号)。所以退役必须跟版本抬升同一次落地,⛔ 不能先删列。⇒ 正在发的 17.4.0(精确 1.7.2)不受影响,照发。 这张卡是后续,不是发布阻塞。
Generated by Claude Code
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.
Generated by Claude Code
四维分析(PM 席位,2026-09-10)
补上这张决策卡缺的四棱块。四条里有两条是立卡后新量到的,都指向同一个方向。
一 · 业务影响:
issuer现在是净负债,不是净资产它被引进来是为了安全(区分「谁担保了这个账号 id」),但实际付出的代价是静默锁死。
showcase-demo-personas-loginable.dogfood.test.ts的头注把它写死了:a credential row whose
issueris not the local credential issuer is invisible tofindAccountByKey— sign-in then failsINVALID_EMAIL_OR_PASSWORDbehind a "User not found" warn that points at thesys_userrow, which is fine, instead of at the account, which is not. Four checklist items had that recorded as a knownGap, each rediscovering it.⭐ 四个 checklist 项各自独立踩进同一个坑,各自记成 knownGap。 一个字段能让四拨人分别重新发现同一个陷阱,说明它的失效方式是不可见的:报错指向
sys_user(看着合理),真正坏掉的是sys_account(没人看那里)。1.7.3 里
issuer不参与键,这一整类 bug 随之消失。这是采纳回滚最实在的收益,比「跟上游」有分量得多。二 · 安全价值:基本是空的,残留只有一种情况
sys_sso_provider的索引是{ fields: ['provider_id'], unique: true }—— 环境内唯一。所以provider_id → issuer是一个函数,(provider_id, account_id)已经能确定(issuer, account_id)所确定的同一件事。⇒
issuer在键里多出来的区分度,在我们自己的数据模型下几乎为零。⚠️ 残留的唯一真实情况:同一个provider_id的注册被改指到另一个 IdP。此时老行与新行 issuer 不同,1.7.3 的键会把它们判成同一个账号。这条必须单独接住,见「四」。三 · 技术成本:比卡里写的小
卡把 hash-shadow-key(#11627 / PR #12198)算作沉没投资。实测不是为这个索引专建的:
sql-driver-11627-hash-shadow-key.test.ts测的是驱动层通用能力(「任意 >768 字符列上的 UNIQUE」「恰好在 MAX_KEYABLE_VARCHAR_CHARS+1 处切换」「复合元组、NULL 保持互不冲突」),服务的是 >768 类的 5 个 UNIQUE 成员。⇒ 删掉这一个索引不浪费那块地基,其余四个成员照用。
另外我们不直接调
findAccountByKey(全树只有注释提到它),耦合点比看起来少。四 · 风险与时机:真风险在迁移,不在决定
⛔ 迁移动的是身份表,做错了的形态是「所有人登不进来」,这是这件事唯一需要小心的地方。而最具体的那个危险,卡里没点명:
唯一性从
(issuer, account_id)变成(provider_id, account_id),会改变哪些行算冲突。 如果现有数据里存在两行provider_id+account_id相同、仅issuer不同 —— 在旧键下完全合法 —— 那么迁移一加新唯一约束就会撞上。这正是「二」里那个残留情况在数据里的样子。⇒ 所以迁移的第一步必须是探测而不是改写:先查有没有这种行,有多少,再决定怎么处置。⛔ 不许先加约束看它炸。
不做的代价在涨:钉死在 1.7.2 拿不了安全补丁,而这一家人只能整体移动(
@better-auth/core@1.7.2与@better-auth/kysely-adapter@1.7.3双向不兼容)。结论
建议选一(采纳上游回滚),理由按权重排:① 它删掉一整类已经咬过四次的静默锁死;②
issuer的区分度在我们的模型下本就接近零;③ 成本比卡里估的小。⛔ 但有一个不可省的前置条件:执行卡必须先做重复行探测,并把「provider 改指到另一个 IdP」的处置写进验收(改指时强制重建账号绑定,而不是指望键去挡)。不写清就删列,等于把一个已知的窄风险变成无人看守。
选三只是延后并让差距变大;选二是为一个几乎为零的区分度,永久维护一个作者本人已经放弃的模型。
Ruled: option 1 — adopt the rollback. Maintainer, 2026-09-10, 「同意」 to the four-dimension analysis above.
Execution card: #17440, dispatched. It carries the two findings that decided it (the four-times-rediscovered silent lockout, and
provider_idbeing unique per environment), and two constraints the original card did not have:- ⭐ a preflight that detects the rows which are legal under
(issuer, account_id)and collide under(provider_id, account_id), and refuses — ⛔ rather than adding the constraint and letting apply blow up as the detection mechanism - the re-pointed-provider case must be answered and pinned in that PR, not left implicit — it is the one situation where
issuerstill discriminates, and dropping the column while ignoring it converts a known narrow risk into an unguarded one
This card stays open until #17440 lands, then closes with it.
- ⭐ a preflight that detects the rows which are legal under
Landed — closing with #17440
PR #17454 merged as
9bd4344e4, verified an ancestor oforigin/main. Option 1 is in the tree:sys_account.issuerand its(issuer, account_id)unique index are gone,backfill-account-issuer.tsandaccount-issuer-parity.test.tsare retired, and the@better-auth/*family moved as one line to an exact1.7.3.Both conditions this decision was made conditional on were met, not waived:
- ⭐ The pre-flight reads ROWS, never the declaration. That turned out to matter more than the analysis anticipated:
(provider_id, account_id)had been declared UNIQUE since the object was created, long predatingissuer— but declared is not present. On a database where that index never materialised, dropping the column does not fail loudly, it degrades silently: indistinguishable rows, an arbitraryfindAccountByKeyresolution, sign-in landing on the wrong user. The four-dimension analysis predicted a migration that fails on duplicates; the real hazard was quieter and worse, and the shipped probe is built for the real one. - ⭐ The re-pointed provider is answered and pinned —
refuseIssuerRepointWithLiveBindings, on the single-update door and onupdateMany, because a re-point smuggled through the bulk door is the same re-point. Guarding one and leaving the other is the classic hole here.
The ceremony was reused rather than invented: ADR-0131 D10 → ADR-0120 D4, through the
os migratefamily, with the refusal placed below the report and above both writes.⛔ What this card does not claim: that
issuerwas worthless. It was the right thing to adopt when better-auth 1.7 keyed on it; what made it a net liability here was thatsys_sso_provider.provider_idis unique per environment, so its discriminating power was already near zero in this data model — plus a failure mode (silent lockout behind an error pointing at the wrong row) that four separate checklist items each rediscovered.Follow-up carried forward, not lost: #17453 (three approvals-checklist knownGap texts still cite the retired backfill), graded
finding, ⛔ deliberately not ridden along on the p1.- ⭐ The pre-flight reads ROWS, never the declaration. That turned out to matter more than the analysis anticipated:
- added a commit that references this issue
on Sep 17, 2026
The durable half of #16186, which shipped the stopgap:
@objectstack/plugin-authnow pins thebetter-authfamily to an exact1.7.2, which restores a working fresh install but freezes the family on a line upstream has already moved off.What upstream actually did
@better-auth/core@1.7.3did not renamecreateLocalAccountIssuer— it removed the issuer-scoped account identity outright (better-auth/better-auth#10909, merged 2026-08-24). Measured by diffing the two publishedsrctrees:db/schema/account.ts— theissuerfield is gone fromaccountSchema;AccountKeyreverts fromPick(BaseAccount, "issuer" | "accountId")toPick(BaseAccount, "providerId" | "accountId");createLocalAccountIssuerandcreateOAuthAccountIssuerare deleted.db/get-tables.ts— theaccount.issuercolumn and theunique (issuer, accountId)index are both gone.oauth2/oauth-provider.ts— theaccountIssueroption is gone, and with it the per-provider declarations (google,apple,line,facebook,cognito,paybin, and the Entra per-login resolver).accountIssueroccurs zero times in the whole 1.7.3 package.So there is no drop-in replacement to adopt.
findAccountByKeykeys on(providerId, accountId)again.What adopting it costs here
Adoption is not a range bump. It removes a required column from a platform object and needs a migration for every existing deployment:
packages/platform-objects/src/identity/sys-account.object.ts—issueris a declared field; the four generated translation bundles carry its label.packages/plugins/plugin-auth/src/backfill-account-issuer.ts(plus its test, 44 references) — the boot-time pass that stampssys_account.issuerexists only to serve the 1.7 identity model. It retires whole.packages/plugins/plugin-auth/src/account-issuer-parity.test.ts— pins our issuer derivation against@better-auth/core/social-providers, a surface 1.7.3 no longer has.auth-schema-config.ts,managed-extension-fields.tsandauth-manager.tsmap the column into better-auth's schema.local:credentialand provider issuers under a unique(issuer, account_id)index. Dropping the column changes which rows collide.The decision this needs
sys_account.issuer, retire the backfill, migrate existing rows, and raise the family floor to1.7.3. Matches upstream; costs a schema migration on the identity table.issueras an application-owned additional field and resolve accounts on it ourselves rather than throughfindAccountByKey. Keeps the data model; owns a divergence from the vendor forever.@better-auth/coreto 1.7.3, which droppedcreateLocalAccountIssuer— a freshobjectstack dev --seed-adminnever creates the system tables and never seeds #16186 leaves behind. Correct today; the pin cannot take a security patch without a reviewed lift, and the gap widens with every upstream release.Whichever is chosen, the family still moves as one line:
@better-auth/core@1.7.2and@better-auth/kysely-adapter@1.7.3are mutually incompatible in both directions (1.7.2 hascreateLocalAccountIssuerand lackschecksSchema; 1.7.3 is the reverse).What already guards the choice
pnpm check:vendor-export-contract(added by #16186) fails on any lift whose new version drops a symbol our shipped source imports, and its--resolveleg enumerates every registry version the declared range admits. So this decision cannot be taken by accident, only deliberately.Filed by the developer seat that shipped the #16186 stopgap. Not claimed.