Skip to content

feat(core): serve auth API routes from the adapter manifest - #203

Merged
Bccorb merged 1 commit into
mainfrom
feat/manifest-driven-proxy
Oct 8, 2026
Merged

Bccorb merged 1 commit into
mainfrom
feat/manifest-driven-proxy

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Closes #201. Part of fells-code/seamless-auth-api#371.

Why

Every API route needed a handler here, an entry in the ensureCookies table, and a route in each of Express, Fastify and Next.js. A route missing from any of them answered the adapter's own 404, and nothing caught it. As a result, several API routes were never reachable through an adapter:

  • TOTP sign-in (POST /totp/verify-login)
  • POST /registration/phone and /registration/phone/verify
  • POST /admin/users/import
  • On Next.js, the PUT OAuth provider retirement routes

The auth API now publishes which token each route takes and which tokens its response issues or clears (fells-code/seamless-auth-api#380). The adapters can follow that instead of a hand-maintained list, which is also what the Go, Rust and Python adapters will build on.

What changes

Core

  • createAdapterManifestSource fetches /.well-known/seamless-adapter.json once.
    • It waits at most 5 seconds, keeps the manifest for the life of the process, and falls back to a copy bundled with the package.
    • After a failure it retries a minute later rather than on every request.
    • A manifest with a credential or effect this version does not understand is refused whole.
  • matchManifestRoute matches static segments case-insensitively and prefers a static segment over a parameter. It refuses a parameter that decodes to . or .., so a request cannot send the held token to a different upstream path.
  • handleManifestRoute proxies a matched route:
    • It requires and sends the held token the route names. In bearer transport it sends the client's own token instead.
    • It stores the session a response issues, after verifying it against JWKS, and only when the response actually carries one.
    • It clears what the route clears, keeps token and refreshToken out of cookie-transport bodies, and handles external delivery.
    • It streams responses that are not JSON.
  • ensureCookies takes optional method and manifest, and then loads the cookie the manifest names. The existing table still covers routes the manifest does not list.
  • scripts/sync-adapter-manifest.mjs regenerates the bundled copy from the API.

Adapters. Each one sends any manifest route without a handler of its own through handleManifestRoute, after the origin guard and cookie loading:

  • Express: a fallback after all routes.
  • Fastify: a wildcard route for GET, POST, PUT, PATCH and DELETE. Static routes win over it, and OPTIONS is left to the application's CORS handling. Manifest routes are served whatever their path casing, as Express and Next.js already did.
  • Next.js: a fallback in dispatch, plus a PUT handler.

Every handler already here still serves its route, and every export stays, so this is not breaking. Dedicated handlers can shrink in follow-ups where the manifest grows to cover what they do (refresh, /users/me, logout's 204, the delivery routes). The changeset is minor for all four packages.

Visible to adopters

  • One extra request. Each adapter now calls the auth API for the manifest on its first request. Adopter test suites that mock fetch will see it, and fetchManifest: false uses only the bundled copy. Existing tests here pass that option so they stay deterministic.
  • Next.js routes need PUT. The catch-all route should export it: export const { GET, POST, PUT, PATCH, DELETE } = createSeamlessAuthHandler(...).

Follow-ups

Checks

  • pnpm test: core 395, express 210, fastify 150, nextjs 159, all passing (846 before this change).
  • pnpm check:types-current passes.
  • New tests:
    • Core: manifest parsing, matching, loading, timeout and fallback; the proxy handler; manifest-aware ensureCookies.
    • Express: route tests.
    • Fastify and Next.js: parity cases against Express for TOTP sign-in, an undeclared access route, PUT, 404, and a route only the live manifest knows.
  • A security review of the diff found no issues. It suggested the dot-segment check, which is included.

The adapters hard-coded every API route: a handler, an ensureCookies table
entry, and a route in each framework. A route missing from any of them
answered the adapter's own 404, so TOTP sign-in, the phone registration
steps and bulk user import were never reachable, and Next.js had no PUT.

Core now loads the adapter manifest the auth API publishes at
/.well-known/seamless-adapter.json, falling back to a bundled copy, and
handleManifestRoute proxies any route it lists: it sends the held token the
route names, stores the session it issues after verifying it, clears what
it clears, keeps tokens out of cookie-transport bodies, and handles external
delivery. ensureCookies takes the route's credential from the manifest.
Express, Fastify and Next.js use it for every route without a handler of
their own, so existing routes behave as before. Next.js also returns PUT.

A path parameter that decodes to a dot segment does not match, so it cannot
send the held token to a different upstream path.

Closes #201.
@Bccorb
Bccorb merged commit 7f2e490 into main Oct 8, 2026
4 checks passed
@Bccorb
Bccorb deleted the feat/manifest-driven-proxy branch October 8, 2026 12:00
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.

feat(core): drive the proxy and cookie handling from the API adapter manifest

1 participant