Skip to content

fix(auth): type the registration ttl as a number - #27

Merged
Bccorb merged 1 commit into
mainfrom
fix/registration-ttl-is-a-number
Jul 31, 2026
Merged

fix(auth): type the registration ttl as a number#27
Bccorb merged 1 commit into
mainfrom
fix/registration-ttl-is-a-number

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

First of three. Fixes a real failure reported testing the Fastify API template.

The bug

Registration fails on Fastify with:

TypeError: option maxAge is invalid: 300
    at Module.stringifySetCookie (/app/node_modules/cookie/src/index.ts:305:13)
    at serialize (/app/node_modules/@fastify/cookie/index.js:16:23)

cookie checks Number.isInteger(maxAge). 300 as a number passes, so the value reaching it is the string "300" (the error interpolates without quotes).

Why the contract is at fault

RegistrationSuccessSchema.ttl was z.string(), describing what the API happened to send rather than what the value is. Every other ttl in this file is z.number(), which made this the only one a consumer could not treat like the rest.

The adapters then diverged on it:

Adapter What it does Result
Express maxAge: maxAgeSeconds * 1000 "300" * 1000 === 300000, the multiply coerces, works
Fastify maxAge: maxAgeSeconds string reaches cookie, Number.isInteger fails, throws

So the Express adapter has been masking this the whole time.

One thing I checked that is not affected: signSessionCookie builds expiresIn: \${ttlSeconds}s`, so "300"interpolates to"300s"` exactly as the number would. No JWT lifetime bug, the damage is confined to the Fastify cookie throw.

Versioning

Marked minor, not major. This is a breaking contract change, but the package is pre-1.0 and a major here would mint 1.0.0, which this does not warrant. The changeset says plainly that it breaks anyone reading the field as a string.

Tests

Three added: a numeric ttl parses, a string ttl is rejected the way LoginSuccessResponseSchema already rejects one, and the field stays optional.

Verification

npm run typecheck, npm test (162 passing), npm run lint, npm run format:check all clean.

The rest of the fix

  • seamless-auth-api: stop sending '300' (blocked on this release, since response bodies are validated against this schema at runtime)
  • seamless-auth-server: parse the value at core's untyped upstream boundary, so no adapter can diverge on it again

RegistrationSuccessSchema.ttl was z.string(), describing what the auth API sent
rather than what the value is. It was the only ttl in the auth schemas a
consumer could not treat like the others.

That had a real consequence. The Fastify adapter hands the value to a cookie
library that requires an integer, so registration failed there with
`TypeError: option maxAge is invalid: 300`. The Express adapter multiplies it
into milliseconds, which coerces the string, so the same response worked and the
mismatch stayed hidden.

Breaking for anyone reading the field as a string. Taken as a minor because the
package is pre-1.0.

Verified with npm run typecheck, npm test (162 passing), npm run lint, and
npm run format:check.
@Bccorb
Bccorb merged commit dbee1a4 into main Jul 31, 2026
1 check passed
@Bccorb
Bccorb deleted the fix/registration-ttl-is-a-number branch July 31, 2026 17:27
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.

1 participant