feat(fastify): add the Fastify adapter - #132
Merged
Merged
Conversation
Adds @seamless-auth/fastify, serving the same routes as the Express adapter and issuing the same cookies. Registered under a prefix, so Fastify's encapsulation keeps the cookie and origin hooks off the rest of the application. requireAuth and requireRole ship as preHandler hooks. Writing a second adapter is what the epic in #72 was for, and it found three things core still left to each adapter. The last guard decisions move into core alongside checkProxyIdentity: checkOrigin, authenticateCookie and authorizeRoles each take the request facts and return a GuardRejection or nothing. Express now calls them too, which is a straight substitution. SetCookieCommand and ClearCookieCommand gain an expires. The command previously specified only a max age, so two adapters could satisfy it and still emit different headers: Express sent Expires and Max-Age, Fastify sent Max-Age alone, which older clients treat as a session cookie. The parity suite caught it. ensureCookies matches a mount-relative path. Express hands a mounted router a req.path with the mount point already stripped, Fastify does not, so the prefix has to come off explicitly or every route silently loses its cookie payload. Also caught by the parity suite. The parity suite runs the same requests through both adapters against the same mocked auth API and asserts status, body and every Set-Cookie header match, so the two cannot drift. Refs #72
The types package already defines it, field for field. Declaring it again here is the duplication #118 and #120 were about. Re-exported under the same name from core and the adapters, so nothing changes for adopters. The re-export is type-only, so it is erased at compile time: the emitted core still imports only @seamless-auth/types/role/matching and neither zod nor the schema barrel enters the runtime graph.
Copied from the express package.json, the version said 0.10.0, which would have published a first release claiming the history of a package that has been shipping for ten minors. Set to 0.0.0 so the pending minor changeset releases it as 0.1.0. The package is deliberately outside the core/express linked group, so it versions on its own until it has the same track record.
The errorCode/errorBody split was marked major, which changesets takes literally and would have published core and express as 1.0.0. For a 0.x package the breaking bump is the minor, and the changesets README still describes these as pre-1.0. Marked minor, so the release is 0.11.0. The changeset still leads with BREAKING and the migration guidance, and now says why a breaking change is shipping as a minor.
6 tasks
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 the open question on #72: whether core is actually strong enough for a second adapter.
What this adds
@seamless-auth/fastify, serving the same routes as the Express adapter and issuing the same cookies. Register it under a prefix; Fastify's encapsulation keeps the cookie and origin hooks off the rest of the application.requireAuthandrequireRoleship aspreHandlerhooks, alongsidegetSeamlessUser.Roughly half the code for the same surface. That difference is the epic's payoff, not cleverness here: cookie signing, failure rendering, proxying, delivery, and the guard decisions all come from core now.
What building it found
This is the part worth reviewing. Three things core still left to each adapter, all caught by the parity suite rather than by reading.
1. Cookie lifetime was underspecified.
SetCookieCommandcarried onlymaxAgeSeconds. Express emittedExpiresandMax-Age; Fastify emittedMax-Agealone, which older clients treat as a session cookie. Both satisfied the type. The command now carries anexpires, and clearing carries the epoch, so the same instruction produces the same header everywhere.2.
ensureCookiestakes a mount-relative path, and nothing says so. It matches the request path against its own route table withstartsWith. Express hands a mounted router areq.pathwith the mount point already stripped; Fastify'sreq.urlkeeps the prefix. Passing/auth/organizationsinstead of/organizationsmatched nothing, so every route silently lost its cookie payload and the proxy gate 401'd. Silent, not loud, which is the bad kind.3. The last guard decisions were still adapter-side.
checkOrigin,authenticateCookie, andauthorizeRolesnow joincheckProxyIdentityin core: each takes the request facts and returns aGuardRejectionor nothing. Express calls them too, as a straight substitution. Without this the Fastify adapter would have copied ~200 lines of security-relevant decision logic, which is the duplication the epic exists to remove.Also filed separately: #131, a route param interpolated unencoded into the upstream URL in the Express adapter. Writing the 27 proxy routes as a declarative table centralized the encoding, which made the two hand-written exceptions visible.
Parity is enforced, not claimed
packages/fastify/tests/parity.test.jsruns the same requests through both adapters against the same mocked auth API and asserts the status, body, and everySet-Cookieheader match: 23 cases covering passthrough and coded failures, the OAuth siblingcode, admin and metrics and session and system-config routes, identity rejections, cookie set and clear, four cookie policies, repeated query parameters, route param encoding, and the cross-site block.It is not decorative. It failed 12 of 23 on the first run and found both contract gaps above.
Scope
createSeamlessConsoleProxyhas no Fastify equivalent yet. It proxies the admin console's static assets and is a separate concern from the auth routes. Noted in the README.Checks
pnpm buildclean.pnpm testpasses: 357 tests across the three packages (189 core, 145 express, 23 fastify).Follow-up worth filing
The admin, metrics, session, and system-config routes are pure "call a core handler, apply the result" boilerplate: 520 lines in Express, 136 here as a table. Core could own that table outright and both adapters would shrink again. I did not do it in this PR because it changes the Express adapter substantially and this one is already large.