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
13 changes: 13 additions & 0 deletions .changeset/public-system-config-response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@seamless-auth/types': minor
---

Add `PublicSystemConfigResponseSchema`, the slice of the system configuration a signed-out client
may read.

It carries `loginMethods` and nothing else. The sign-in screens in the SDKs currently fall back to a
hardcoded list of methods when they have no session, which can advertise a method an instance has
turned off. A contract for the configured methods lets a client ask instead of guess, and lets it
decide whether declining a passkey during registration would leave the user with no way back in.

Additive. No existing schema, type, or export changes.
27 changes: 27 additions & 0 deletions src/schemas/systemConfig/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DefaultLockoutPolicy,
OAuthProviderConfigSchema,
OAuthProviderUpdateSchema,
PublicSystemConfigResponseSchema,
SystemConfigPatchSchema,
SystemConfigSchema,
createPatchSystemConfigSchema,
Expand Down Expand Up @@ -135,3 +136,29 @@ describe('createPatchSystemConfigSchema', () => {
).toThrow();
});
});

describe('PublicSystemConfigResponseSchema', () => {
it('accepts the configured login methods', () => {
expect(() =>
PublicSystemConfigResponseSchema.parse({ loginMethods: ['passkey', 'magic_link'] }),
).not.toThrow();
});

it('rejects an empty method list, which would leave a client with nothing to offer', () => {
expect(() => PublicSystemConfigResponseSchema.parse({ loginMethods: [] })).toThrow();
});

it('rejects a method outside the enum', () => {
expect(() => PublicSystemConfigResponseSchema.parse({ loginMethods: ['password'] })).toThrow();
});

it('drops any config key that is not part of the public slice', () => {
const parsed = PublicSystemConfigResponseSchema.parse({
loginMethods: ['passkey'],
rpid: 'localhost',
origins: ['http://localhost:5173'],
});

expect(parsed).toEqual({ loginMethods: ['passkey'] });
});
});
19 changes: 19 additions & 0 deletions src/schemas/systemConfig/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,22 @@ export const AvailableRolesResponseSchema = z.object({
});

export type AvailableRolesResponse = z.infer<typeof AvailableRolesResponseSchema>;

/**
* The slice of the system configuration a signed-out client may read.
*
* Served unauthenticated so the bundled sign-in screens can match how an
* instance is actually configured before anyone has a session, rather than
* guessing at a default set of methods. It also tells a client whether declining
* a passkey during registration would leave the user with no way back in, which
* is the difference between offering a skip and trapping someone out of their
* own account.
*
* Everything else in SystemConfigSchema stays behind the admin routes. Only add
* a key here when a signed-out client genuinely cannot work without it.
*/
export const PublicSystemConfigResponseSchema = z.object({
loginMethods: z.array(LoginMethodSchema).min(1),
});

export type PublicSystemConfigResponse = z.infer<typeof PublicSystemConfigResponseSchema>;
Loading