You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Observation: MetadataProvider's seed principal collapses to @anon in the window where the bearer is absent, so two principals can share one seed scope #5746
Recorded while implementing #5664 (PR #5744). Static reading, not reproduced in a browser — filed so triage can decide whether it is worth measuring, not as a claimed defect.
principalScope() is the half of the seed key that makes a objectui:metadata: entry
unreadable by a different principal — the property pinned by MetadataProvider.crossPrincipalSeed.test.tsx as "is unreadable by the next principal even
when the purge never ran" (#5198). When TokenStorage.get() answers null that
discrimination collapses: every such session computes the same @anon scope, so one
session's permission-filtered app list is readable by the next.
When the bearer can be absent
createAuthClient.getSession() writes TokenStorage from payload.session.token on every
boot, so for a returning browser the token is normally present. Two paths that reach principalScope() with it absent:
MetadataProvider reads the seed synchronously at mount, before getSession() has
resolved. On a browser whose stale token was just purged by apps/console/src/lib/auth-preflight.ts (its documented job) there is nothing to read
and the mount computes @anon.
A deployment or sign-in path whose session is established by cookie alone, which auth-preflight.ts's own header calls out: "Cookie-based sign-in (better-auth sets a
session cookie) does NOT touch this localStorage key."
What is not claimed
The window is short, and it may be unreachable in practice on every path the console
actually takes — that is the part that needs measuring rather than reasoning. Enumerating
what the app writes across a real sign-in (the method #5664 was worked with) would settle
it in one pass.
PR #5744 makes a change of session user drop the previous user's client state wholesale,
including a sessionStorage sweep. That closes the cross-user half of this on any boot
where the session user actually changes. What it does not close is two sessions sharing @anon inside a single page-load, or a browser whose session-user pointer never got
written.
The reading is static but the stake is cross-principal readability of a permission-filtered cache (objectui:metadata: seed under a shared @anon scope), which is exactly the class #5198 pinned against — so "worth measuring" is a yes, and establish-first is the queued scope:
Measure the two named windows with the same enumeration method Cross-user client-state pollution: previous user's workspace shown to a new user, and the guided first-run flow suppressed #5664 used (what the app writes across a real sign-in): (a) mount after auth-preflight.ts purges a stale token, before getSession() resolves; (b) a cookie-only session (better-auth cookie set, TokenStorage never written). The question each time: does a objectui:metadata:@anon:* seed get written in that window, and can a subsequent different-principal session read it before purge?
latent-not-live is a legal terminal: if neither path writes a readable seed in practice, report that with the enumeration attached and close — no speculative hardening.
Claim — domain:ui execution seat. PM round 28, session session_01EuPCi56cnGyykygi3z9w4m, branch claude/issue-5746-anon-seed-scope-measurement. Dispatching at default tier per the triage grading's own size/model note — this is a measurement card, not a fix card, and it is dispatched as one.
The establish-first scope is the triage comment's three steps, unchanged. Two points carried into the dispatch as hard constraints so the shape cannot drift:
latent-not-live is a legal terminal. If neither named window writes a readable objectui:metadata:@anon:* seed in practice, the deliverable is that finding with the enumeration attached — and the card closes. ⛔ No speculative hardening of a window that was measured shut.
Sequencing note: PR #5816 (#5777, per-key try in purgeSignedOutClientCaches) is in the merge queue and touches the sign-out sweep of this same objectui:metadata: prefix. Different file, different half — but re-measure on a main that includes it rather than on a base that predates it.
Terminal: latent-not-live — both named windows measure SHUT
Measured on origin/main @ ad404e057. Sequencing note first: PR #5816 (#5777, per-key try in purgeSignedOutClientCaches) had not landed when I started and has still not — it is an open draft, not in the merge queue, base ad404e057. So I measured on a base that does not include it. That is harmless here: #5816 touches the sign-out sweep in packages/auth/src/AuthProvider.tsx, and every reading below is about the mount half (read/write of the seed key), which #5816 does not touch. Nothing in this enumeration depends on it.
Method
The whole console mount path is real — the real AuthProvider (so the getSession() → TokenStorage.set → setIsLoading(false) ordering is the shipped one), the real ConnectedShell session gate, the real MetadataProvider. Only two boundaries are doubled, and they are the two a test cannot have: the auth server (an AuthClient double) and the metadata server (the adapter). Nothing writes objectui:metadata:* by hand, so the harness cannot agree with a key format the provider does not actually produce.
Storage.prototype.{setItem,getItem,removeItem} is instrumented to record, for every objectui:metadata:* operation: the key, its principal segment, TokenStorage.get()at that instant, and whether getSession() had already answered.
Counter-probe (S0) — and it earned its keep. The first run reported zero operations across every scenario. Not a finding: jsdom hands out sessionStorage as a Proxy, so inside a Storage.prototype method this is the unproxied target and this === sessionStorage is never true. S0 failed, which is what it is for; the instrument was repointed and S0 now records the authenticated path's non-@anon write.
The enumeration
===== LEDGER: S0 authenticated boot (counter-probe) =====
#1 get objectui:metadata:app:org_a:163ikz60d0vmvq
principal=163ikz60d0vmvq bearerAtOp=tok-alice getSessionResolved=true MISS
#2 set objectui:metadata:app:org_a:163ikz60d0vmvq
principal=163ikz60d0vmvq bearerAtOp=tok-alice getSessionResolved=true items=2
===== LEDGER: S1 stale-token-purged boot, cookie session, getSession delayed 60ms =====
#1 get objectui:metadata:app:org_a:0f53rp80tub9eu
principal=0f53rp80tub9eu bearerAtOp=tok-alice-fresh getSessionResolved=true MISS
#2 set objectui:metadata:app:org_a:0f53rp80tub9eu
principal=0f53rp80tub9eu bearerAtOp=tok-alice-fresh getSessionResolved=true items=2
S1: total metadata ops=2 writes=1 anon-writes=0 reads=1
===== LEDGER: S2 cookie-only boot (session response carries no token) =====
#1 get objectui:metadata:app:org_a:@anon
principal=@anon bearerAtOp=null getSessionResolved=true MISS
#2 set objectui:metadata:app:org_a:@anon
principal=@anon bearerAtOp=null getSessionResolved=true items=3
S2: writes=1 anon-writes=1 keys=["objectui:metadata:app:org_a:@anon"]
===== LEDGER: S3 user B boots in the same tab (B fetch never lands) =====
#3 get objectui:metadata:app:org_a:@anon
principal=@anon bearerAtOp=null getSessionResolved=true HIT len=54
S3 VERDICT: B rendered apps="setup,crm,hr-secret" loading=false
seed-reads=["objectui:metadata:app:org_a:@anon -> HIT len=54"]
storage-now=[]
===== LEDGER: S4 auth-disabled guest boot =====
#1 get objectui:metadata:app:@none:@anon
principal=@anon bearerAtOp=null getSessionResolved=false MISS
#2 set objectui:metadata:app:@none:@anon
principal=@anon bearerAtOp=null getSessionResolved=false items=1
===== LEDGER: S5 preview-mode boot =====
#1 get objectui:metadata:app:@none:@anon
principal=@anon bearerAtOp=null getSessionResolved=false MISS
#2 set objectui:metadata:app:@none:@anon
principal=@anon bearerAtOp=null getSessionResolved=false items=1
Window (a) — mount before getSession() resolves: SHUT, and the premise is false
S1 runs the realpreflightAuth() against a server that rejects the stale bearer (it purges: localStorage['auth-session-token'] → null, asserted), then boots with getSession() deliberately delayed 60 ms. Result: anon-writes=0, and both metadata operations carry bearerAtOp=tok-alice-fresh getSessionResolved=true.
The card's premise — "MetadataProvider reads the seed synchronously at mount, before getSession() has resolved" — is not true of the console. MetadataProviderdoes not exist in that window. ConsoleShell.ConnectedShellInner holds the #4042 session gate:
if (!adapter || isAuthLoading) return <LoadingFallback />;
and isLoading only drops in loadSession's finally, which runs aftercreateAuthClient.getSession() has already executed TokenStorage.set(payload.session.token). There is no ordering in which the provider mounts first. ConnectedShell is the only mount site of <MetadataProvider> in this repo.
Window (b) — cookie-only session with TokenStorage never written: SHUT, because the premise does not hold
This one reduces entirely to a server-side fact: does a cookie-only get-session response carry session.token? If it does, createAuthClient.getSession() writes TokenStorage on the first boot and the scope is a real fingerprint.
I measured it directly against better-auth v1.6.28 (the pinned dependency), memory adapter, bearer() enabled — signed up to obtain a session cookie, then called get-session with only that cookie and no Authorization header:
Corroborated in library source: @better-auth/core's sessionSchema declares token: z.ZodString — required and non-nullable.
So the auth-preflight.ts sentence the card quotes — "Cookie-based sign-in ... does NOT touch this localStorage key" — is true of the sign-in action and does not survive the first getSession(), which every boot performs before the metadata tree is allowed to mount.
What IS live if the premise ever holds (S2 + S3) — the conditional, stated honestly
S2/S3 model the counterfactual: a session response with no token, so TokenStorage stays empty while an authenticated, permission-filtered session is in hand. There, the disclosure is complete, not marginal:
S2 writes objectui:metadata:app:org_a:@anon with A's 3-app permission-filtered list.
So the seed key's principal scope is the only thing standing between latent and live, exactly as the card reasoned. It just never collapses on a real session.
Correction to an assumption carried in triage and dispatch
The dispatch states that #5744's wholesale drop "already covers the cross-user-boot half". Measured, it does not cover the seed read. S3's storage-now=[] shows the sweep did run — but it ran after B had already read and rendered A's list. React runs child effects before parent effects, and MetadataProvider (child, newly mounted in the commit where isLoading flips) reads the seed before AuthProvider's SessionUserScope.adopt(user.id) effect fires purgePreviousUserClientState(). What actually covers that boot is the principal-scoped key from #5198 — not the purge. Worth recording so a future change does not retire the key on the strength of the purge.
Residual: two synthetic-identity boots that DO write @anon (not named on the card)
S4 (enabled={false} guest) and S5 (previewMode) both write objectui:metadata:app:@none:@anon. Both set isLoading false immediately and never touch TokenStorage, so principalScope() is @anon for the entire session.
This is not a disclosure, and the reason is a reasoned judgement about the server, not something I measured: ConditionalAuthWrapper selects those two branches only when discovery reports auth as stub/handlerReady:false or the deployment is a marketplace preview — i.e. precisely when the server is not filtering /meta/* per user, so there is no per-principal payload to leak. I did not stand up such a deployment to confirm the server side. apps/console never takes either branch (App.tsx mounts <AuthProvider authUrl={AUTH_URL}> unconditionally), and no in-repo consumer composes ConditionalAuthWrapper with ConnectedShell — the combination is reachable only for a downstream consumer, since both are public exports of @object-ui/app-shell.
Measured vs. reasoned — the boundary
Measured: everything in the ledgers above; the real preflightAuth purge; better-auth's cookie-only get-session body; the child-before-parent effect ordering (via S3's read-then-sweep); the instrument's own liveness (S0).
Reasoned, not measured: that the S4/S5 deployments have no per-user permission filtering server-side; that no downstream consumer mounts <MetadataProvider> outside ConnectedShell (I verified only this repo).
Disposition
No fix is proposed and no PR is opened — nothing here is a defect to harden against, and hardening a window measured shut is what the card scoped out. Branch claude/issue-5746-anon-seed-scope-measurement (6573e1098) carries the harness at packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx as inspectable evidence only — it is deliberately not proposed for merge; if it is wanted on main as a standing pin, that is a separate call. It runs green as committed:
pnpm exec vitest run packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx --maxWorkers=2
Test Files 1 passed (1)
Tests 6 passed (6)
node scripts/check-control-bytes.mjs ✅ · node scripts/check-changeset-presence.mjs ✅ (empty-frontmatter changeset, test-only) · eslint --no-inline-config on the harness → 0 errors, 0 warnings. All on 6573e1098.
{
"issue": 5746,
"status": "done",
"terminal": "latent-not-live",
"branch": "claude/issue-5746-anon-seed-scope-measurement",
"pr": null,
"premise_still_valid": false,
"summary": "Measurement card, terminal latent-not-live. Both named windows measure SHUT. Window (a) premise is FALSE: MetadataProvider does not mount before getSession() resolves — ConnectedShellInner's #4042 session gate (`if (!adapter || isAuthLoading) return <LoadingFallback/>`) withholds it, and isLoading only drops in loadSession's finally, after createAuthClient.getSession() has already run TokenStorage.set(payload.session.token). With getSession deliberately delayed 60ms on a browser whose stale token the REAL preflightAuth() had just purged: anon-writes=0, every metadata op carried bearerAtOp=tok-alice-fresh. Window (b) premise does not hold: measured against better-auth v1.6.28 (the pinned dep) with a cookie and NO bearer, get-session returns 200 with session.token present as a string (session keys: createdAt,expiresAt,id,ipAddress,token,updatedAt,userAgent,userId); @better-auth/core's sessionSchema declares token: z.ZodString, required. So a cookie-only session writes TokenStorage on the first boot, before the metadata tree is allowed to mount. The auth-preflight sentence the card quotes is true of the sign-in ACTION and does not survive the first getSession(). Landed NO hardening and opened NO PR, per the card's own scope. Two things worth carrying forward: (1) IF TokenStorage were ever empty on an authenticated session the disclosure is COMPLETE — S2 wrote objectui:metadata:app:org_a:@anon and S3 had a different user render 'setup,crm,hr-secret' (including hr-secret) with loading=false; (2) CORRECTION to an assumption in triage/dispatch: #5744's wholesale drop does NOT cover the seed read — S3 shows the sweep ran AFTER B had already read and rendered A's list, because React runs child effects (MetadataProvider's seed read) before parent effects (AuthProvider's SessionUserScope.adopt purge). What covers that boot is #5198's principal-scoped key, not the purge.",
"tests": "All on committed head 6573e1098 (branch pushed; NO PR opened). Enumeration harness at packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx renders the REAL mount path (real AuthProvider + real ConnectedShell session gate + real MetadataProvider); only the auth server and metadata server are doubled; nothing writes objectui:metadata:* by hand. Storage.prototype.{setItem,getItem,removeItem} instrumented to record key, principal segment, TokenStorage.get() at that instant, and whether getSession() had answered. COUNTER-PROBE (S0) earned its keep: the FIRST run reported zero operations in every scenario — jsdom hands out sessionStorage as a Proxy, so `this === sessionStorage` is never true inside a Storage.prototype method; the instrument was repointed at the key prefix and S0 now records the authenticated path's non-@anon write (principal=163ikz60d0vmvq, 14 base-36 chars, items=2). A second instrument defect was caught the same way: S3 installed its never-settling adapter by REASSIGNING fakeAdapter.getClient, which vi.restoreAllMocks() does not undo, so S4's first reading ran against S3's adapter and was void — replaced with a reset flag and re-measured. `pnpm exec vitest run packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx --maxWorkers=2` (from repo root, under the shared verify lock) => 'Test Files 1 passed (1) / Tests 6 passed (6)'; enumeration output byte-identical to the pre-commit run. `node scripts/check-control-bytes.mjs` => '✅ check-control-bytes: OK (scanned 4844 tracked text file(s); skipped 85 binary).' `node scripts/check-changeset-presence.mjs` => '✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' (empty frontmatter — objectui declares test-only that way; there is no skip-changeset label in this repo). `eslint --no-inline-config --format json` on the harness => files linted: 1, errors=0, warnings=0. Exit codes captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own printed line. Separate probe (run, then deleted, not committed): a real better-auth v1.6.28 instance with memoryAdapter + bearer(), signed up for a cookie, then get-session with cookie only => 'get-session status: 200 / body.session.token present: true / typeof: string'. DECLARED NARROWING: I did not run `pnpm --filter @object-ui/app-shell type-check`, which would have required building the dependency closure under a contended shared lock; the file is test-only, is not proposed for merge, lints clean and runs green. CI would run the full farm if this were ever proposed as a PR.",
"open_questions": [],
"out_of_scope_findings": []
}
Terminal: latent-not-live. Both named windows measure SHUT. Closing per the triage ruling, which made this a legal terminal — ⛔ no speculative hardening landed, and no PR was opened.
(a) "MetadataProvider reads the seed synchronously at mount, before getSession() resolves" — premise is FALSE.ConsoleShell.ConnectedShellInner holds the #4042 session gate (if (!adapter || isAuthLoading) return <LoadingFallback />), and isLoading only drops in loadSession's finally, which runs aftercreateAuthClient.getSession() has already executed TokenStorage.set(payload.session.token). The metadata tree is not allowed to mount before the bearer exists. Measured with the REAL preflightAuth() purging a stale token and getSession deliberately delayed 60 ms: anon-writes = 0, and every metadata storage op carried a real bearer with getSessionResolved=true.
(b) "a cookie-only session never writes TokenStorage" — does not hold either. Measured against better-auth v1.6.28 (the pinned dep), driven with a session cookie and no bearer: get-session answers 200 with session.token present as a string, and @better-auth/core's sessionSchema declares token: z.ZodString — required, non-nullable. So a cookie-only session writes TokenStorage on its first boot, before the metadata tree may mount. The auth-preflight.ts sentence the card quotes is true of the sign-in action and does not survive the first getSession().
⚠️ CORRECTION — this one matters more than the terminal
Both the triage comment above and the dispatch brief asserted that #5744's wholesale drop covers the cross-user half. It does not cover the seed READ.
Measured: S3's storage-now=[] shows the sweep ran — but it ran after B had already read and rendered A's list. React runs child effects before parent effects, and MetadataProvider's seed read is a child effect newly mounted in the very commit where isLoading flips, while AuthProvider's SessionUserScope.adopt → purgePreviousUserClientState is the parent effect. The purge is strictly later than the read on that boot.
So what protects that boot is #5198's principal-scoped key, and nothing else. Recorded explicitly so that a future change does not retire the key on the strength of the purge — the purge is not a substitute for it, and the reasoning that says otherwise is now falsified in a measurement rather than merely doubted.
The conditional is complete rather than marginal: with TokenStorage empty on an authenticated session, S2 wrote objectui:metadata:app:org_a:@anon, and S3 — a different user booting in the same tab — HIT that key and rendered setup,crm,hr-secret, including hr-secret, with loading=false. The key is the only thing standing between latent and live; it simply never collapses on a real session.
Residual, outside this card's two windows — filed separately, not decided here
Two synthetic-identity boots DO write @anon: a guest boot (enabled={false}) and previewMode, both writing objectui:metadata:app:@none:@anon. The dev judged these non-disclosures because ConditionalAuthWrapper selects those branches only when discovery reports an auth stub / handlerReady:false, or the deployment is a marketplace preview — i.e. when the server is not filtering /meta/* per user. ⚠️That server-side judgement is REASONED, not measured, and was flagged as such rather than presented as a result. apps/console never takes either branch. Filed as a separate finding for triage.
Instrument notes — the counter-probe earned its keep twice
Recorded because both failures would have produced a falselatent-not-live:
jsdom hands out sessionStorage as a Proxy, so this === sessionStorage is never true inside a Storage.prototype method. The first run therefore reported zero operations in every scenario — which, without the counter-probe, reads exactly like a clean "no writes anywhere" result. Repointed at the key prefix, the probe now records the authenticated path's non-@anon write.
vi.restoreAllMocks() does not undo a reassignment.S3 installed a never-settling adapter by reassigning fakeAdapter.getClient, so S4 silently ran against S3's adapter and reported writes=[]. Replaced with a reset flag and re-measured, after which S4 does write.
The harness (packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx) renders the real console mount path — real AuthProvider, real ConnectedShell session gate, real MetadataProvider; only the two servers are doubled, and nothing writes objectui:metadata:* by hand, so it cannot agree with a key format the provider does not produce. It lives on branch claude/issue-5746-anon-seed-scope-measurement, unmerged. Given the correction above — the safety property rests on ONE mechanism — landing it as a regression pin is worth considering by whoever takes the residual; that is a recommendation, not a decision taken here.
Sequencing note
Measured on origin/main @ ad404e057. The dispatch brief said PR #5816 was in the merge queue; at measurement time it was still an open draft, and the dev said so rather than assuming the brief was right. Harmless here — #5816 touches the sign-out sweep, every reading above is the mount half. (#5816 has since merged, 13:41:41Z.)
Closing as completed: the card asked whether the window is reachable, and it is not.
Activity
os-zhuang commentedon Aug 23, 2026
Triage (first-touch): promoted
pm:queue,domain:ui(kept), +security, typeTask— measurement card.The reading is static but the stake is cross-principal readability of a permission-filtered cache (
objectui:metadata:seed under a shared@anonscope), which is exactly the class #5198 pinned against — so "worth measuring" is a yes, and establish-first is the queued scope:auth-preflight.tspurges a stale token, beforegetSession()resolves; (b) a cookie-only session (better-auth cookie set,TokenStoragenever written). The question each time: does aobjectui:metadata:@anon:*seed get written in that window, and can a subsequent different-principal session read it before purge?latent-not-liveis a legal terminal: if neither path writes a readable seed in practice, report that with the enumeration attached and close — no speculative hardening.Size/model suggestion:S–M measurement-heavy, opus.Generated by Claude Code
claude commentedon Aug 23, 2026
Claim —
domain:uiexecution seat. PM round 28, sessionsession_01EuPCi56cnGyykygi3z9w4m, branchclaude/issue-5746-anon-seed-scope-measurement. Dispatching at default tier per the triage grading's own size/model note — this is a measurement card, not a fix card, and it is dispatched as one.The establish-first scope is the triage comment's three steps, unchanged. Two points carried into the dispatch as hard constraints so the shape cannot drift:
latent-not-liveis a legal terminal. If neither named window writes a readableobjectui:metadata:@anon:*seed in practice, the deliverable is that finding with the enumeration attached — and the card closes. ⛔ No speculative hardening of a window that was measured shut.Sequencing note: PR #5816 (#5777, per-key
tryinpurgeSignedOutClientCaches) is in the merge queue and touches the sign-out sweep of this sameobjectui:metadata:prefix. Different file, different half — but re-measure on amainthat includes it rather than on a base that predates it.Generated by Claude Code
test(app-shell): enumerate the @anon seed-scope windows (#5746)
os-zhuang commentedon Aug 23, 2026
Terminal:
latent-not-live— both named windows measure SHUTMeasured on
origin/main@ad404e057. Sequencing note first: PR #5816 (#5777, per-keytryinpurgeSignedOutClientCaches) had not landed when I started and has still not — it is an open draft, not in the merge queue, basead404e057. So I measured on a base that does not include it. That is harmless here: #5816 touches the sign-out sweep inpackages/auth/src/AuthProvider.tsx, and every reading below is about the mount half (read/write of the seed key), which #5816 does not touch. Nothing in this enumeration depends on it.Method
The whole console mount path is real — the real
AuthProvider(so thegetSession()→TokenStorage.set→setIsLoading(false)ordering is the shipped one), the realConnectedShellsession gate, the realMetadataProvider. Only two boundaries are doubled, and they are the two a test cannot have: the auth server (anAuthClientdouble) and the metadata server (the adapter). Nothing writesobjectui:metadata:*by hand, so the harness cannot agree with a key format the provider does not actually produce.Storage.prototype.{setItem,getItem,removeItem}is instrumented to record, for everyobjectui:metadata:*operation: the key, its principal segment,TokenStorage.get()at that instant, and whethergetSession()had already answered.Counter-probe (S0) — and it earned its keep. The first run reported zero operations across every scenario. Not a finding: jsdom hands out
sessionStorageas a Proxy, so inside aStorage.prototypemethodthisis the unproxied target andthis === sessionStorageis never true. S0 failed, which is what it is for; the instrument was repointed and S0 now records the authenticated path's non-@anonwrite.The enumeration
Window (a) — mount before
getSession()resolves: SHUT, and the premise is falseS1 runs the real
preflightAuth()against a server that rejects the stale bearer (it purges:localStorage['auth-session-token']→null, asserted), then boots withgetSession()deliberately delayed 60 ms. Result:anon-writes=0, and both metadata operations carrybearerAtOp=tok-alice-fresh getSessionResolved=true.The card's premise — "
MetadataProviderreads the seed synchronously at mount, beforegetSession()has resolved" — is not true of the console.MetadataProviderdoes not exist in that window.ConsoleShell.ConnectedShellInnerholds the #4042 session gate:and
isLoadingonly drops inloadSession'sfinally, which runs aftercreateAuthClient.getSession()has already executedTokenStorage.set(payload.session.token). There is no ordering in which the provider mounts first.ConnectedShellis the only mount site of<MetadataProvider>in this repo.Window (b) — cookie-only session with
TokenStoragenever written: SHUT, because the premise does not holdThis one reduces entirely to a server-side fact: does a cookie-only
get-sessionresponse carrysession.token? If it does,createAuthClient.getSession()writesTokenStorageon the first boot and the scope is a real fingerprint.I measured it directly against better-auth v1.6.28 (the pinned dependency), memory adapter,
bearer()enabled — signed up to obtain a session cookie, then calledget-sessionwith only that cookie and noAuthorizationheader:Corroborated in library source:
@better-auth/core'ssessionSchemadeclarestoken: z.ZodString— required and non-nullable.So the
auth-preflight.tssentence the card quotes — "Cookie-based sign-in ... does NOT touch this localStorage key" — is true of the sign-in action and does not survive the firstgetSession(), which every boot performs before the metadata tree is allowed to mount.What IS live if the premise ever holds (S2 + S3) — the conditional, stated honestly
S2/S3 model the counterfactual: a session response with no token, so
TokenStoragestays empty while an authenticated, permission-filtered session is in hand. There, the disclosure is complete, not marginal:objectui:metadata:app:org_a:@anonwith A's 3-app permission-filtered list.setup,crm,hr-secret— A's list, includinghr-secret— withloading=false.So the seed key's principal scope is the only thing standing between latent and live, exactly as the card reasoned. It just never collapses on a real session.
Correction to an assumption carried in triage and dispatch
The dispatch states that #5744's wholesale drop "already covers the cross-user-boot half". Measured, it does not cover the seed read. S3's
storage-now=[]shows the sweep did run — but it ran after B had already read and rendered A's list. React runs child effects before parent effects, andMetadataProvider(child, newly mounted in the commit whereisLoadingflips) reads the seed beforeAuthProvider'sSessionUserScope.adopt(user.id)effect firespurgePreviousUserClientState(). What actually covers that boot is the principal-scoped key from #5198 — not the purge. Worth recording so a future change does not retire the key on the strength of the purge.Residual: two synthetic-identity boots that DO write
@anon(not named on the card)S4 (
enabled={false}guest) and S5 (previewMode) both writeobjectui:metadata:app:@none:@anon. Both setisLoadingfalse immediately and never touchTokenStorage, soprincipalScope()is@anonfor the entire session.This is not a disclosure, and the reason is a reasoned judgement about the server, not something I measured:
ConditionalAuthWrapperselects those two branches only when discovery reports auth asstub/handlerReady:falseor the deployment is a marketplace preview — i.e. precisely when the server is not filtering/meta/*per user, so there is no per-principal payload to leak. I did not stand up such a deployment to confirm the server side.apps/consolenever takes either branch (App.tsxmounts<AuthProvider authUrl={AUTH_URL}>unconditionally), and no in-repo consumer composesConditionalAuthWrapperwithConnectedShell— the combination is reachable only for a downstream consumer, since both are public exports of@object-ui/app-shell.Measured vs. reasoned — the boundary
preflightAuthpurge; better-auth's cookie-onlyget-sessionbody; the child-before-parent effect ordering (via S3's read-then-sweep); the instrument's own liveness (S0).<MetadataProvider>outsideConnectedShell(I verified only this repo).Disposition
No fix is proposed and no PR is opened — nothing here is a defect to harden against, and hardening a window measured shut is what the card scoped out. Branch
claude/issue-5746-anon-seed-scope-measurement(6573e1098) carries the harness atpackages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsxas inspectable evidence only — it is deliberately not proposed for merge; if it is wanted onmainas a standing pin, that is a separate call. It runs green as committed:node scripts/check-control-bytes.mjs✅ ·node scripts/check-changeset-presence.mjs✅ (empty-frontmatter changeset, test-only) ·eslint --no-inline-configon the harness → 0 errors, 0 warnings. All on6573e1098.Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
{ "issue": 5746, "status": "done", "terminal": "latent-not-live", "branch": "claude/issue-5746-anon-seed-scope-measurement", "pr": null, "premise_still_valid": false, "summary": "Measurement card, terminal latent-not-live. Both named windows measure SHUT. Window (a) premise is FALSE: MetadataProvider does not mount before getSession() resolves — ConnectedShellInner's #4042 session gate (`if (!adapter || isAuthLoading) return <LoadingFallback/>`) withholds it, and isLoading only drops in loadSession's finally, after createAuthClient.getSession() has already run TokenStorage.set(payload.session.token). With getSession deliberately delayed 60ms on a browser whose stale token the REAL preflightAuth() had just purged: anon-writes=0, every metadata op carried bearerAtOp=tok-alice-fresh. Window (b) premise does not hold: measured against better-auth v1.6.28 (the pinned dep) with a cookie and NO bearer, get-session returns 200 with session.token present as a string (session keys: createdAt,expiresAt,id,ipAddress,token,updatedAt,userAgent,userId); @better-auth/core's sessionSchema declares token: z.ZodString, required. So a cookie-only session writes TokenStorage on the first boot, before the metadata tree is allowed to mount. The auth-preflight sentence the card quotes is true of the sign-in ACTION and does not survive the first getSession(). Landed NO hardening and opened NO PR, per the card's own scope. Two things worth carrying forward: (1) IF TokenStorage were ever empty on an authenticated session the disclosure is COMPLETE — S2 wrote objectui:metadata:app:org_a:@anon and S3 had a different user render 'setup,crm,hr-secret' (including hr-secret) with loading=false; (2) CORRECTION to an assumption in triage/dispatch: #5744's wholesale drop does NOT cover the seed read — S3 shows the sweep ran AFTER B had already read and rendered A's list, because React runs child effects (MetadataProvider's seed read) before parent effects (AuthProvider's SessionUserScope.adopt purge). What covers that boot is #5198's principal-scoped key, not the purge.", "tests": "All on committed head 6573e1098 (branch pushed; NO PR opened). Enumeration harness at packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx renders the REAL mount path (real AuthProvider + real ConnectedShell session gate + real MetadataProvider); only the auth server and metadata server are doubled; nothing writes objectui:metadata:* by hand. Storage.prototype.{setItem,getItem,removeItem} instrumented to record key, principal segment, TokenStorage.get() at that instant, and whether getSession() had answered. COUNTER-PROBE (S0) earned its keep: the FIRST run reported zero operations in every scenario — jsdom hands out sessionStorage as a Proxy, so `this === sessionStorage` is never true inside a Storage.prototype method; the instrument was repointed at the key prefix and S0 now records the authenticated path's non-@anon write (principal=163ikz60d0vmvq, 14 base-36 chars, items=2). A second instrument defect was caught the same way: S3 installed its never-settling adapter by REASSIGNING fakeAdapter.getClient, which vi.restoreAllMocks() does not undo, so S4's first reading ran against S3's adapter and was void — replaced with a reset flag and re-measured. `pnpm exec vitest run packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx --maxWorkers=2` (from repo root, under the shared verify lock) => 'Test Files 1 passed (1) / Tests 6 passed (6)'; enumeration output byte-identical to the pre-commit run. `node scripts/check-control-bytes.mjs` => '✅ check-control-bytes: OK (scanned 4844 tracked text file(s); skipped 85 binary).' `node scripts/check-changeset-presence.mjs` => '✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' (empty frontmatter — objectui declares test-only that way; there is no skip-changeset label in this repo). `eslint --no-inline-config --format json` on the harness => files linted: 1, errors=0, warnings=0. Exit codes captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own printed line. Separate probe (run, then deleted, not committed): a real better-auth v1.6.28 instance with memoryAdapter + bearer(), signed up for a cookie, then get-session with cookie only => 'get-session status: 200 / body.session.token present: true / typeof: string'. DECLARED NARROWING: I did not run `pnpm --filter @object-ui/app-shell type-check`, which would have required building the dependency closure under a contended shared lock; the file is test-only, is not proposed for merge, lints clean and runs green. CI would run the full farm if this were ever proposed as a PR.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 23, 2026
Terminal:
latent-not-live. Both named windows measure SHUT. Closing per the triage ruling, which made this a legal terminal — ⛔ no speculative hardening landed, and no PR was opened.domain:uiexecution seat, PM round 28, sessionsession_01EuPCi56cnGyykygi3z9w4m.The two windows
(a) "MetadataProvider reads the seed synchronously at mount, before
getSession()resolves" — premise is FALSE.ConsoleShell.ConnectedShellInnerholds the #4042 session gate (if (!adapter || isAuthLoading) return <LoadingFallback />), andisLoadingonly drops inloadSession'sfinally, which runs aftercreateAuthClient.getSession()has already executedTokenStorage.set(payload.session.token). The metadata tree is not allowed to mount before the bearer exists. Measured with the REALpreflightAuth()purging a stale token andgetSessiondeliberately delayed 60 ms: anon-writes = 0, and every metadata storage op carried a real bearer withgetSessionResolved=true.(b) "a cookie-only session never writes
TokenStorage" — does not hold either. Measured against better-auth v1.6.28 (the pinned dep), driven with a session cookie and no bearer:get-sessionanswers 200 withsession.tokenpresent as a string, and@better-auth/core'ssessionSchemadeclarestoken: z.ZodString— required, non-nullable. So a cookie-only session writesTokenStorageon its first boot, before the metadata tree may mount. Theauth-preflight.tssentence the card quotes is true of the sign-in action and does not survive the firstgetSession().Both the triage comment above and the dispatch brief asserted that #5744's wholesale drop covers the cross-user half. It does not cover the seed READ.
Measured:
S3'sstorage-now=[]shows the sweep ran — but it ran after B had already read and rendered A's list. React runs child effects before parent effects, andMetadataProvider's seed read is a child effect newly mounted in the very commit whereisLoadingflips, whileAuthProvider'sSessionUserScope.adopt → purgePreviousUserClientStateis the parent effect. The purge is strictly later than the read on that boot.So what protects that boot is #5198's principal-scoped key, and nothing else. Recorded explicitly so that a future change does not retire the key on the strength of the purge — the purge is not a substitute for it, and the reasoning that says otherwise is now falsified in a measurement rather than merely doubted.
The conditional is complete rather than marginal: with
TokenStorageempty on an authenticated session,S2wroteobjectui:metadata:app:org_a:@anon, andS3— a different user booting in the same tab — HIT that key and renderedsetup,crm,hr-secret, includinghr-secret, withloading=false. The key is the only thing standing between latent and live; it simply never collapses on a real session.Residual, outside this card's two windows — filed separately, not decided here
Two synthetic-identity boots DO write⚠️ That server-side judgement is REASONED, not measured, and was flagged as such rather than presented as a result.
@anon: a guest boot (enabled={false}) andpreviewMode, both writingobjectui:metadata:app:@none:@anon. The dev judged these non-disclosures becauseConditionalAuthWrapperselects those branches only when discovery reports an auth stub /handlerReady:false, or the deployment is a marketplace preview — i.e. when the server is not filtering/meta/*per user.apps/consolenever takes either branch. Filed as a separate finding for triage.Instrument notes — the counter-probe earned its keep twice
Recorded because both failures would have produced a false
latent-not-live:sessionStorageas a Proxy, sothis === sessionStorageis never true inside aStorage.prototypemethod. The first run therefore reported zero operations in every scenario — which, without the counter-probe, reads exactly like a clean "no writes anywhere" result. Repointed at the key prefix, the probe now records the authenticated path's non-@anonwrite.vi.restoreAllMocks()does not undo a reassignment.S3installed a never-settling adapter by reassigningfakeAdapter.getClient, soS4silently ran againstS3's adapter and reportedwrites=[]. Replaced with a reset flag and re-measured, after whichS4does write.The harness (
packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx) renders the real console mount path — realAuthProvider, realConnectedShellsession gate, realMetadataProvider; only the two servers are doubled, and nothing writesobjectui:metadata:*by hand, so it cannot agree with a key format the provider does not produce. It lives on branchclaude/issue-5746-anon-seed-scope-measurement, unmerged. Given the correction above — the safety property rests on ONE mechanism — landing it as a regression pin is worth considering by whoever takes the residual; that is a recommendation, not a decision taken here.Sequencing note
Measured on
origin/main@ad404e057. The dispatch brief said PR #5816 was in the merge queue; at measurement time it was still an open draft, and the dev said so rather than assuming the brief was right. Harmless here — #5816 touches the sign-out sweep, every reading above is the mount half. (#5816 has since merged, 13:41:41Z.)Closing as
completed: the card asked whether the window is reachable, and it is not.Generated by Claude Code
test(app-shell): pin the @anon metadata seed scope enumeration (#5828)
test(app-shell): pin the @anon metadata seed scope enumeration (objec…