Repository navigation
Description
Observation-class finding, measured while implementing #8144 (PR #8244). Not caused by that change — it is a pre-existing ordering property that #8144 made visible because it put a row on the ledger at exactly that moment.
What was measured
databaseHooks.session.create.before derives a session's activeOrganizationId from the caller's sys_member row (ADR-0081 D1, AuthManager.composeDatabaseHooks → defaultActiveOrg). The membership itself is written by the ADR-0093 reconciler composed into user.create.after, and better-auth defers that past the signup transaction — membership-actor-attribution.dogfood.test.ts already polls up to 10s for it, and calls the delay out in a comment.
So the session minted by sign-up is created before the user has any membership. It carries activeOrganizationId: null, legitimately: at that instant there is no organization to name.
Reproduced while writing #8144's dogfood test: signing up a member and immediately reading the login audit row for that first session yields tenant_id: null (and organization_id: null). Waiting for the sys_member row before signing in produces a correctly tenanted row on every subsequent session. The test now measures the tenant half on an ordinary sign-in and documents the first-session case rather than asserting on it.
Why it is worth a card
The SecurityPlugin's RLS predicate gates on organization_id. audit-writers.ts already names the consequence in its own comment, for the sibling cause:
Without this fallback, audit rows are written with
organization_id=NULLand the SecurityPlugin's RLS predicate (organization_id = current_user.organization_id) hides them forever — making the audit log UI appear permanently empty even though writes succeed.
Its fallback reads the audited record's own organization_id, which cannot help here: a session row has no organization column either, and the account/user rows being written in that same window are in the same state.
The window is narrow (a user's first session) but the loss is permanent — nothing back-fills a written ledger row — and the affected rows are exactly the ones describing account creation.
Related, not duplicate
- A record an admin creates over HTTP is immediately invisible to its own creator when the caller has no active organization and the Layer 0 wall is active (
organization_idstamped NULL) #8208 — a record created with no active organization is invisible to its own creator (same RLS consequence, different producer: an HTTP write rather than the signup ordering). - plugin-sharing: a
manage_sharingholder whose session has no ACTIVE organization reads every tenant's sharing rules (adminOrgScope falls open) #8158 —adminOrgScopefalls open when a session has no active organization (same "no active org" precondition, opposite failure direction).
This card is about the ordering that produces the no-active-org state for every new user, and about ledger rows specifically.
Not proposed here
Whether the fix is to settle the membership before minting the first session, to back-fill the tenant on the audit row when the session later gains one, or to accept it and document it, is a design call with a real cost on the sign-up hot path. No fix is implied by this filing.
Source: #8144 / PR #8244, packages/qa/dogfood/test/auth-session-audit-trail.dogfood.test.ts (see the beforeAll note).
Activity
os-zhuang commentedon Aug 12, 2026
交叉引用:本卡是「已认证会话但无 active organization」这一族的第三张,而三张的方向各不相同。
domain:identity执行席位 #6022 汇总,sessionsession_01PEVB6w7D7uCszR9Mw1BL73。⛔ 不定级、不改标签 —— 只是把这个聚簇钉在一起,免得三张卡被当成三件无关的事各自处理。同一个前提 —— 一个已认证的会话,没有 active organization —— 在三个子系统里落成了三种不同、且互相矛盾的行为,全部在本班实测得到:
plugin-sharing的adminOrgScopemanage_sharing者读到所有租户的共享规则(实测 HTTP 200,两个租户的行);evaluateRule还会跨租户写。已修(PR #8237)organization_id: NULL,随后对创建者自己不可见tenant_id为 NULL,对 RLS 谓词永久不可见为什么这个聚簇值得一起看
user.create.after推迟到注册事务之后),不是任何一处的判断失误。audit-log (A): addlogin/logoutwriters on the auth session hooks, and attribute the unattributedlast_login_atupdate row #8144 没有引入它,只是第一次让它在账本上留下可见痕迹。本席位不做的事
⛔ 不合并这三张卡、不提议统一方案、不定级。"无 active org 的会话该是什么"是平台语义问题,属于 maintainer 层;三张卡各自的局部处置也已分别记录(#8158 已修并说明了为何选"拒绝"而非"返回空",#8208 与本卡未取处置)。
这里只保证一件事:下一个读到其中任何一张的人,能看见另外两张。
Refs: #8158(已修,PR #8237)、#8208、#8144 / PR #8244(本卡的观察来源)。
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Triage: routed
domain:identity(audit writers + auth session ordering:plugin-audit/plugin-auth's ADR-0093 reconciler seam). Type Bug — the ledger writes rows its declared readers can never see, and the loss is permanent.Held as
finding; disposition is gated on #8247, the decision card this round filed over the cluster the seat comment above pins (#8158 fell open / #8208 falls closed silently / this card writes permanently-invisible rows — three improvised answers to the one undeclared state). Re-grade trigger: #8247 ruled. If the ruling includes the targeted signup-ordering fix (option C there), this card likely closes into that implementation; if it rules pure fail-closed (B), this card becomes the ledger carve-out question and should be re-graded rather than closed.Generated by Claude Code
hotlong commentedon Aug 13, 2026
Disposition from the #8247 ruling (maintainer, 2026-08-13, option C — see the ruling comment there): audit rows are the carve-out the ruling exists for. The signup-ordering half (membership settles synchronously before the first session mints, or first-session ledger rows' tenant is back-filled once the session gains its org) is precisely the remedy for the permanently-invisible NULL-tenant audit rows this finding measured. In scope of the #8247 implementation card; the
domain:identityseat should treat this finding as absorbed by that dispatch, and its acceptance should verify a new user's first-session audit rows are tenant-visible after the fix.Generated by Claude Code
hotlong commentedon Aug 13, 2026
State-machine migration, maintainer-authorized (2026-08-13, live triage session: 「我授权你先迁标签」; semantics on #8449): disposition recorded above — absorbed by the #8247 option-C implementation (signup-ordering / back-fill half), verified at its acceptance — so
finding→pm:on-hold. Disposition, verification obligation, domain unchanged.Generated by Claude Code
fix(auth): settle the membership before the first session resolves it…
docs(adr): anchor the default-active-org session hook to ADR-0093 D9 (o…