Skip to content

finding(app-shell): buildExpressionUser's signed-in branch forwards id/name/email unguarded, so a session missing one yields the present-and-undefined shape #5424 barred #6551

Description

@claude

Found while implementing objectui#6534. Filed unassigned, recording only — not graded, no type.

Measured on claude/issue-6534-anonymous-user-id, which is origin/main @ bf3a03c1d plus that card's fix. The observation is older than that branch and unaffected by it: it lives in the normaliser's SIGNED-IN branch, which objectui#6534 does not touch.

What was measured

objectui#6534 closed the anonymous branch's missing id by giving it id: null. The signed-in branch beside it forwards three keys raw, out of a cast that declares all three optional:

constu=useras|{id?: string;name?: string;email?: string;role?: string;[key: string]: unknown}|null|undefined;return{id: u.id,// ← `undefined` when the session has no `id`name: u.name,// ← sameemail: u.email,// ← samerole: u.role??'user',isPlatformAdmin: u.isPlatformAdmin??false,positions: u.positions??[],};

So buildExpressionUser({ name: 'B', email: 'b@c.d' }) returns an object where 'id' in built === true and built.id === undefinedpresent-and-always-undefined, which is precisely the shape objectui#5424 removed roles from this same object for: "the shape that teaches the wrong thing", where the context answers rather than being plainly absent and the answer is silently wrong.

The bottom three keys already defend against this with ??; the top three do not. That asymmetry is inside one object literal.

Why this is recorded and not fixed

It is not reachable today, as far as anything measured shows. The only production input is useAuth().user, a better-auth session principal that always carries id; packages/auth/src/types.ts derives it from the spec's AuthUser, whose id is required. So this is a latent shape hazard, not a live fail-open — unlike objectui#6534, which was reproduced on screen.

And the fix is a decision, not a mechanical edit. At least three defensible shapes, and they disagree about what a session with no id even IS:

  • id: u.id ?? null — consistent with the anonymous branch, but it is a FALLBACK in the consumer, which is the thing #0.1 and objectui#6534's own scope fence both forbid; it also silently equates "signed in, no id" with "signed out".
  • Treat a session with no id as anonymous (fall to the !u branch) — arguably the honest reading, but it changes which branch runs for a real input and needs a census of what actually reaches the normaliser.
  • Narrow the cast so id is required and let the producer be wrong loudly — contract-first, and the direction AGENTS.md #0.1 points; costs a type change on a shared normaliser.

objectui#6534 was fenced as "a tightening and only a tightening" with the shape settled in advance, so widening it into this would have been out of scope in two directions at once. Filed separately rather than folded in.

Related

  • objectui#6534 — closed the same class on the ANONYMOUS branch; this is the residue its fix made visible by fixing the other half.
  • objectui#5424 — set the "no present-and-undefined key" precedent on this exact object, and is the reason undefined is not an acceptable answer here either.

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions