From e39584ece94e16192fc740e4beb2c18bcce77f61 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 30 Jul 2026 16:54:39 -0400 Subject: [PATCH] feat(system-config): add a public system config response contract The sign-in screens in the SDKs have no session when they render, so they fall back to a hardcoded list of login methods. That list can advertise a method an instance has turned off, and it is a second source of truth for something the server already knows. PublicSystemConfigResponseSchema is the slice of the configuration a signed-out client may read. It carries loginMethods and nothing else, so a client can ask which methods an instance offers rather than guess, and can tell whether declining a passkey during registration would leave a user with no way back in. Additive. No existing schema, type, or export changes. --- .changeset/public-system-config-response.md | 13 ++++++++++ src/schemas/systemConfig/schema.test.ts | 27 +++++++++++++++++++++ src/schemas/systemConfig/schema.ts | 19 +++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .changeset/public-system-config-response.md diff --git a/.changeset/public-system-config-response.md b/.changeset/public-system-config-response.md new file mode 100644 index 0000000..496f8ac --- /dev/null +++ b/.changeset/public-system-config-response.md @@ -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. diff --git a/src/schemas/systemConfig/schema.test.ts b/src/schemas/systemConfig/schema.test.ts index 9f709d7..9df2286 100644 --- a/src/schemas/systemConfig/schema.test.ts +++ b/src/schemas/systemConfig/schema.test.ts @@ -3,6 +3,7 @@ import { DefaultLockoutPolicy, OAuthProviderConfigSchema, OAuthProviderUpdateSchema, + PublicSystemConfigResponseSchema, SystemConfigPatchSchema, SystemConfigSchema, createPatchSystemConfigSchema, @@ -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'] }); + }); +}); diff --git a/src/schemas/systemConfig/schema.ts b/src/schemas/systemConfig/schema.ts index 032c16c..a050fac 100644 --- a/src/schemas/systemConfig/schema.ts +++ b/src/schemas/systemConfig/schema.ts @@ -194,3 +194,22 @@ export const AvailableRolesResponseSchema = z.object({ }); export type AvailableRolesResponse = z.infer; + +/** + * 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;