Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/registration-ttl-is-a-number.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@seamless-auth/types': minor
---

`RegistrationSuccessSchema.ttl` is a number, matching every other `ttl` in the contract.

It was `z.string()`, which described what the auth API sent rather than what the value is. That made
it the only `ttl` in the auth schemas a consumer could not treat like the others, and it had a real
consequence: the Fastify adapter passes the value straight 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.

This is a breaking contract change, taken as a minor because the package is pre-1.0. It breaks anyone
reading `RegistrationSuccessResponse['ttl']` as a string, or parsing a registration body that still
carries one. The auth API is fixed in the same round to send a number, and `@seamless-auth/core`
parses the value defensively regardless of what arrives.
21 changes: 21 additions & 0 deletions src/schemas/auth/auth.schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OTPVerifyTokenSuccessSchema,
RefreshSuccessResponseSchema,
RegistrationRequestSchema,
RegistrationSuccessSchema,
} from './auth.schema.js';

describe('LoginRequestSchema', () => {
Expand Down Expand Up @@ -85,3 +86,23 @@ describe('RegistrationRequestSchema', () => {
expect(() => RegistrationRequestSchema.parse({ email: 'nope' })).toThrow();
});
});

describe('RegistrationSuccessSchema', () => {
const base = { message: 'Registration started', sub: 'user-123', token: 't' };

it('accepts a ttl in seconds', () => {
expect(() => RegistrationSuccessSchema.parse({ ...base, ttl: 300 })).not.toThrow();
});

// This field was a string, which is what the API sent. It was the only ttl in
// this file a consumer could not treat like the others, and handing a string
// to a cookie library that requires an integer fails the request.
it('rejects a ttl as a string, the way every other ttl here does', () => {
expect(() => RegistrationSuccessSchema.parse({ ...base, ttl: '300' })).toThrow();
expect(() => LoginSuccessResponseSchema.parse({ message: 'ok', ttl: '300' })).toThrow();
});

it('leaves the ttl optional', () => {
expect(() => RegistrationSuccessSchema.parse(base)).not.toThrow();
});
});
7 changes: 6 additions & 1 deletion src/schemas/auth/auth.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ export const RegistrationSuccessSchema = z.object({
message: z.string(),
sub: z.string().optional(),
token: z.string().optional(),
ttl: z.string().optional(),
// Seconds, as everywhere else a ttl appears in this file. It was a string,
// matching what the API sent, which left this the only ttl in the contract a
// consumer could not treat like the others. A string reaching a cookie library
// that requires an integer is a failed request, so the type states what the
// value has to be rather than what it happened to be.
ttl: z.number().optional(),
delivery: AuthDeliverySchema.optional(),
});

Expand Down