Skip to content

refactor(express,core)!: collapse user.sub into user.id and type getSeamlessUser#92

Merged
Bccorb merged 1 commit into
mainfrom
refactor/collapse-user-sub
Jul 20, 2026
Merged

refactor(express,core)!: collapse user.sub into user.id and type getSeamlessUser#92
Bccorb merged 1 commit into
mainfrom
refactor/collapse-user-sub

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closes #85.

The consumer migrations in seamless-review-api and seamless-portal-api landed first, so removing sub here is a no-op for them.

Part 1: remove the duplicate sub field

SeamlessAuthUser declared both id and sub, and requireAuth populated both from the same access token sub claim. id is the survivor because it is the field common to both user-producing paths: requireAuth yields SeamlessAuthUser, while getSeamlessUser returns the auth API's user object, which has id and no sub.

The two values could not diverge: the auth API sets sub: userId in every token-signing path, and all callers pass user.id.

  • Removed sub from the SeamlessAuthUser interface and from the requireAuth assignment, along with the TODO(#85) marker.
  • Swept the repo for .sub on the user object. The only non-doc hits were tests/requireRole.test.js and an assertion in tests/requireAuth.test.js, both updated. JWT payload sub claims are untouched.

Part 2: type the getSeamlessUser return

getSeamlessUser defaulted its generic to any, returning an untyped object from the upstream /users/me response. That is the root cause of the defensive user.sub ?? user.id coalescing seen in the consumer repos.

Added an exported SeamlessUser interface in core, derived from the auth API's actual response (controller at src/controllers/user.ts, shape enforced by MeResponseSchema in src/schemas/me.response.ts, which strips unknown keys):

export interface SeamlessUser {
  id: string;
  email: string;
  phone: string | null;
  roles: string[];
  lastLogin?: string | null;
  activeOrganizationId?: string | null;
}

lastLogin and activeOrganizationId are modelled optional because the upstream schema marks them so, and both are nullable in practice (lastLogin before the first login, activeOrganizationId when the access token carries no org context). The generic parameter is unchanged for callers passing their own type; only the default moved from any to SeamlessUser. The type is re-exported from the express adapter.

Versioning

Changeset is minor, not major. Both packages are pre-1.0 (core 0.7.0, express 0.8.0), and minor is the 0.x breaking channel. A major bump would ship 1.0.0 and declare API stability, which is a product decision nobody has made. This matches the precedent from #89 (fix(express)!: require Express 5), also changeset'd minor.

Docs

  • Express README now documents the real req.user shape under requireAuth and the accurate getSeamlessUser return type (the previous example was wrong: it listed phone: string and omitted the org and login fields).
  • Added a "Migration: user.sub to user.id" section with the before/after, including collapsing the user.sub ?? user.id fallback.
  • Core README lists getSeamlessUser in the public API overview.

Verification

pnpm build clean across both packages.

pnpm test:

packages/core test:    Test Suites: 15 passed, 15 total
packages/core test:    Tests:       60 passed, 60 total
packages/express test: Test Suites: 21 passed, 21 total
packages/express test: Tests:       82 passed, 82 total

…eamlessUser

SeamlessAuthUser declared both `id` and `sub`, and requireAuth populated
both from the same access token `sub` claim. Only `id` remains, which is
also the identifier returned by getSeamlessUser, so both user-producing
paths now agree on one field name.

getSeamlessUser previously returned `any` by default, which is what made
the defensive `user.sub ?? user.id` coalescing in consumers look
necessary. It now returns the exported SeamlessUser interface, matching
the auth API's GET /users/me response.

Adopters replace `req.user.sub` with `req.user.id`. JWT payload `sub`
claims are unchanged.

Closes #85
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.

refactor(express): collapse duplicated SeamlessAuthUser.id / .sub

1 participant