Skip to content

fix(auth): project sessionFields from the resolved session, not just user - #889

Merged
borisno2 merged 2 commits into
mainfrom
claude/funny-sagan-wrgs1v
Aug 4, 2026
Merged

fix(auth): project sessionFields from the resolved session, not just user#889
borisno2 merged 2 commits into
mainfrom
claude/funny-sagan-wrgs1v

Conversation

@borisno2

@borisno2 borisno2 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • getSessionFromAuth now projects sessionFields off the resolved better-auth session (whatever auth.api.getSession() returns), not just its user sub-object. Resolution precedence for a non-userId field: a top-level key on the resolved session, then user, then session. userId stays special-cased to the authenticated user's id.
  • A customSession shape with no top-level user key (see auth: buildBetterAuthOptions returns the widened BetterAuthOptions, erasing better-auth's plugin/session type inference #876) is now correctly treated as a signed-in session instead of being misreported as anonymous — null means "genuinely no session," nothing else.
  • Errors from the underlying auth.api.getSession() call now propagate instead of being swallowed into null — collapsing a lookup failure into "anonymous" is indistinguishable from a mass sign-out under fail-closed access control.
  • A sessionFields entry that can't be resolved is omitted and logs a warning (once per field, per process) naming what was checked, instead of silently surfacing later as an access-control function reading undefined.
  • The scaffolded getSession() — the CLI feature generator's lib/auth.ts template, and examples/starter-auth/examples/auth-demo — now call this single shared helper, reading sessionFields from the resolved config at runtime instead of baking a field list in at generation time (so editing sessionFields after scaffolding takes effect without regenerating).
  • Fixes examples/auth-demo's getSession(), which previously returned a truthy object of undefined values for an anonymous visitor instead of null.
  • Updated the sessionFields JSDoc and the auth reference doc (docs/content/reference/auth.md) to document the flattened-projection contract, the resolution precedence, and that reconciling a customSession's nested shape is the application's job.

Test plan

  • New/updated tests in packages/auth/tests/server.test.ts cover: the no-user-key customSession case, a session-sub-object field, a deliberate top-level/user/session precedence collision, warn-once-per-field behavior, error propagation vs. genuine null, and the unchanged happy path — 214/214 auth tests pass
  • packages/cli test suite passes (346/346)
  • packages/core test suite passes (948/948)
  • examples/starter-auth and examples/auth-demo regenerate and typecheck cleanly against the updated lib/auth.ts
  • pnpm lint, pnpm manypkg fix, pnpm format
  • Changeset added for @opensaas/stack-auth and @opensaas/stack-cli (minor)

Closes #881


Generated by Claude Code

…user

getSessionFromAuth now reads sessionFields off the resolved better-auth
session (customSession-aware, with a defined top-level/user/session
precedence) instead of only the user object, so a customSession shape with
no `user` key is no longer misreported as anonymous. Errors from the
underlying session lookup now propagate instead of becoming a silent null,
and an unresolvable field warns once instead of vanishing.

The scaffolded getSession() (CLI generator template, starter-auth,
auth-demo) now calls this single helper with sessionFields read from the
resolved config at runtime, fixing auth-demo's truthy-anonymous-session bug
along the way.

Closes #881

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3AJScedpPQTTZbJfbNqfZ
@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 35c75f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@opensaas/stack-auth Minor
@opensaas/stack-cli Minor
@opensaas/stack-core Minor
@opensaas/stack-rag Minor
@opensaas/stack-storage-s3 Minor
@opensaas/stack-storage-vercel Minor
@opensaas/stack-storage Minor
@opensaas/stack-tiptap Minor
@opensaas/stack-ui Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-docs Ready Ready Preview Aug 4, 2026 11:54am

borisno2 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Self-review

Ran an independent review pass against the diff (fresh context, not the authoring session) covering the fix in packages/auth/src/server/index.ts and the three call-site rewrites.

Verdict: no correctness issues found.

  • Resolution precedence (resolveSessionField) is top-level → usersession, with userId special-cased to user.id only (never falling back to the other sources) — verified against the dedicated collision tests.
  • null is returned only when auth.api.getSession() itself is falsy, before ever touching .user — a customSession shape with no user key is projected, not misreported as anonymous.
  • No try/catch remains in getSessionFromAuth — errors from the underlying session lookup propagate, confirmed by a rejects.toThrow test.
  • The warn-once cache is module-level, keyed on field name, matching the existing selectWarnings precedent in packages/core/src/context/index.ts; tests correctly isolate it per-case with vi.resetModules().
  • All three call sites (CLI generator template, starter-auth, auth-demo) read sessionFields from the resolved config at runtime with no leftover hardcoded field list, and auth-demo's old truthy-anonymous-session bug is fixed.
  • The session as unknown as Session | null cast at each call site is necessary (not a smell) given Session is augmented with concrete field types by consuming apps.

Test coverage maps 1:1 to the issue's acceptance criteria (no-user-key customSession, session-only field, real precedence collision, warn-once, error-vs-null, unchanged happy path).


Generated by Claude Code

Comment thread examples/starter-auth/lib/auth.ts Outdated
const authConfig = resolvedConfig._pluginData?.auth as NormalizedAuthConfig | undefined
const sessionFields = authConfig?.sessionFields ?? ['userId', 'email', 'name']
const session = await getSessionFromAuth(auth, sessionFields, await headers())
return session as unknown as Session | null

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a consumer shouldn't need to cast as unknown for the types to be happen - infact a consumer shouldn't need to cast at all

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 35c75f6 — the cast wasn't actually necessary. getSessionFromAuth (packages/auth/src/server/index.ts) now declares its return type as Session | null (from @opensaas/stack-core) instead of Record<string, unknown> | null. Within packages/auth's own build Session is unaugmented, so the internal Record<string, unknown> result satisfies it directly with no cast there either — and because the exported signature is already pinned to Session, every consumer (this file, auth-demo, and the CLI generator's template) now just does return getSessionFromAuth(...) with zero casts, regardless of whatever fields a consuming app's own types/session.d.ts augmentation adds.


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Core Package Coverage (./packages/core)

Status Category Percentage Covered / Total
🟢 Lines 93.32% (🎯 65%) 1231 / 1319
🟢 Statements 91.77% (🎯 65%) 1328 / 1447
🟢 Functions 98.12% (🎯 62%) 209 / 213
🟢 Branches 83.31% (🎯 50%) 894 / 1073
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for UI Package Coverage (./packages/ui)

Status Category Percentage Covered / Total
🔵 Lines 76.72% 244 / 318
🔵 Statements 76.29% 251 / 329
🔵 Functions 69.15% 74 / 107
🔵 Branches 64.25% 160 / 249
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for CLI Package Coverage (./packages/cli)

Status Category Percentage Covered / Total
🔵 Lines 79.16% 1539 / 1944
🔵 Statements 78.86% 1601 / 2030
🔵 Functions 85.94% 214 / 249
🔵 Branches 67.84% 690 / 1017
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Auth Package Coverage (./packages/auth)

Status Category Percentage Covered / Total
🔵 Lines 98.33% 118 / 120
🔵 Statements 98.37% 121 / 123
🔵 Functions 100% 38 / 38
🔵 Branches 94.44% 85 / 90
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/auth/src/config/types.ts 0% 0% 0% 0%
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Package Coverage (./packages/storage)

Status Category Percentage Covered / Total
🔵 Lines 78.57% 220 / 280
🔵 Statements 80.06% 245 / 306
🔵 Functions 86.07% 68 / 79
🔵 Branches 75.88% 214 / 282
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for RAG Package Coverage (./packages/rag)

Status Category Percentage Covered / Total
🔵 Lines 47.97% 355 / 740
🔵 Statements 48.14% 377 / 783
🔵 Functions 54.26% 70 / 129
🔵 Branches 42.55% 180 / 423
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)

Status Category Percentage Covered / Total
🔵 Lines 100% 40 / 40
🔵 Statements 100% 40 / 40
🔵 Functions 100% 9 / 9
🔵 Branches 100% 19 / 19
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)

Status Category Percentage Covered / Total
🔵 Lines 100% 68 / 68
🔵 Statements 100% 71 / 71
🔵 Functions 100% 15 / 15
🔵 Branches 97.87% 46 / 47
File CoverageNo changed files found.
Generated in workflow #1618 for commit 35c75f6 by the Vitest Coverage Report Action

…r casts

Per review feedback on #889: type getSessionFromAuth's return as `Session | null`
(from @opensaas/stack-core) instead of `Record<string, unknown> | null`. Session
is module-augmentable and unaugmented within packages/auth's own build, so the
internal Record<string, unknown> result satisfies it with no cast there — and
because the return type is already pinned to Session, every consumer (the CLI
generator template, starter-auth, auth-demo) can now forward it straight through
with zero casts instead of `as unknown as Session | null`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3AJScedpPQTTZbJfbNqfZ
@borisno2
borisno2 enabled auto-merge (squash) August 4, 2026 11:55
@borisno2
borisno2 merged commit b9b9357 into main Aug 4, 2026
6 checks passed
@borisno2
borisno2 deleted the claude/funny-sagan-wrgs1v branch August 4, 2026 11:59
@github-actions github-actions Bot mentioned this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auth: sessionFields describes a flattened projection of the better-auth user, not the resolved session — mismatch is silent

2 participants