refactor(express,core)!: collapse user.sub into user.id and type getSeamlessUser#92
Merged
Conversation
…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
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #85.
The consumer migrations in
seamless-review-apiandseamless-portal-apilanded first, so removingsubhere is a no-op for them.Part 1: remove the duplicate
subfieldSeamlessAuthUserdeclared bothidandsub, andrequireAuthpopulated both from the same access tokensubclaim.idis the survivor because it is the field common to both user-producing paths:requireAuthyieldsSeamlessAuthUser, whilegetSeamlessUserreturns the auth API's user object, which hasidand nosub.The two values could not diverge: the auth API sets
sub: userIdin every token-signing path, and all callers passuser.id.subfrom theSeamlessAuthUserinterface and from therequireAuthassignment, along with theTODO(#85)marker..subon the user object. The only non-doc hits weretests/requireRole.test.jsand an assertion intests/requireAuth.test.js, both updated. JWT payloadsubclaims are untouched.Part 2: type the
getSeamlessUserreturngetSeamlessUserdefaulted its generic toany, returning an untyped object from the upstream/users/meresponse. That is the root cause of the defensiveuser.sub ?? user.idcoalescing seen in the consumer repos.Added an exported
SeamlessUserinterface in core, derived from the auth API's actual response (controller atsrc/controllers/user.ts, shape enforced byMeResponseSchemainsrc/schemas/me.response.ts, which strips unknown keys):lastLoginandactiveOrganizationIdare modelled optional because the upstream schema marks them so, and both are nullable in practice (lastLoginbefore the first login,activeOrganizationIdwhen the access token carries no org context). The generic parameter is unchanged for callers passing their own type; only the default moved fromanytoSeamlessUser. The type is re-exported from the express adapter.Versioning
Changeset is
minor, notmajor. Both packages are pre-1.0 (core 0.7.0, express 0.8.0), andminoris the 0.x breaking channel. Amajorbump 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
req.usershape underrequireAuthand the accurategetSeamlessUserreturn type (the previous example was wrong: it listedphone: stringand omitted the org and login fields).user.subtouser.id" section with the before/after, including collapsing theuser.sub ?? user.idfallback.getSeamlessUserin the public API overview.Verification
pnpm buildclean across both packages.pnpm test: