From 67fc5210cb39e0ca49b88a067933b6f8010f1bdf Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 30 Jul 2026 22:19:21 -0400 Subject: [PATCH 1/2] feat(passkey): let a user finish registration without a passkey Registration ended on a screen with one control on it. A user who did not want a passkey, or whose device could not make one, had no way forward: the unsupported branch rendered a message and nothing else, on a screen with no exit. The session already exists by then, since the OTP step that leads here establishes it, so leaving without a passkey was always a legitimate way to finish rather than an escape hatch. useLoginMethods reads the instance configuration, and the skip only appears when a method other than passkey is enabled. With passkey as the only one a skip would leave a user unable to sign back into the account they just created, so the control is not rendered at all. Unknown counts as unsafe: a failed or in-flight read shows no skip rather than guessing at one. The unsupported-device branch now says which case it is and offers the same way forward when one exists. Login no longer starts from a hardcoded ['passkey', 'magic_link', 'phone_otp'], which advertised phone OTP on instances that never enabled it. It uses the methods the instance reports, falling back to the narrower set matching the auth server's own defaults. The login response stays authoritative when it carries methods of its own. Requires an auth server serving GET /system-config/public and an adapter that proxies it. Verified with npm run typecheck, npm test (290 passing), npm run lint, npm run format:check, and npm run build, against a local build of the types package. --- .changeset/skip-passkey-registration.md | 25 +++++++ src/client/createSeamlessAuthClient.ts | 15 +++++ src/components/AuthFallbackOptions.tsx | 5 +- src/hooks/useLoginMethods.ts | 74 ++++++++++++++++++++ src/index.ts | 5 ++ src/styles/registerPasskey.module.css | 16 +++++ src/views/Login.tsx | 11 +-- src/views/PassKeyRegistration.tsx | 57 ++++++++++++++-- tests/RegisterPassKey.test.tsx | 89 ++++++++++++++++++++++++- tests/login.test.tsx | 6 ++ tests/useLoginMethods.test.tsx | 81 ++++++++++++++++++++++ 11 files changed, 372 insertions(+), 12 deletions(-) create mode 100644 .changeset/skip-passkey-registration.md create mode 100644 src/hooks/useLoginMethods.ts create mode 100644 tests/useLoginMethods.test.tsx diff --git a/.changeset/skip-passkey-registration.md b/.changeset/skip-passkey-registration.md new file mode 100644 index 0000000..16aa436 --- /dev/null +++ b/.changeset/skip-passkey-registration.md @@ -0,0 +1,25 @@ +--- +'@seamless-auth/react': minor +--- + +Offer a way past passkey registration when another login method is enabled. + +Registration used to end on a screen with one control on it. A user who did not want a passkey, or +whose device could not make one, had no way forward: the unsupported branch rendered a message and +nothing else, on a screen with no exit. The session already exists by then, since the OTP step that +leads here establishes it, so leaving without a passkey was always a legitimate way to finish. + +`useLoginMethods` reads the instance configuration from the auth server, and the skip only appears +when a method other than `passkey` is enabled. With passkey as the only method a skip would leave a +user unable to sign back into the account they just created, so the control is not rendered at all. +Unknown counts as unsafe: a failed or in-flight read shows no skip rather than guessing. + +The unsupported-device branch now says which case it is, and offers the same way forward when one +exists. + +`Login` no longer starts from a hardcoded `['passkey', 'magic_link', 'phone_otp']`. It uses the +methods the instance reports, falling back to the narrower `['passkey', 'magic_link']` that matches +the auth server's own defaults. The login response stays authoritative when it carries methods of +its own. + +Requires an auth server serving `GET /system-config/public`, and an adapter that proxies it. diff --git a/src/client/createSeamlessAuthClient.ts b/src/client/createSeamlessAuthClient.ts index 8cfed63..ebf03c8 100644 --- a/src/client/createSeamlessAuthClient.ts +++ b/src/client/createSeamlessAuthClient.ts @@ -30,6 +30,7 @@ import type { OrganizationMembershipEnvelopeResponse, OrganizationSwitchResponse, PublicOAuthProvider, + PublicSystemConfigResponse, RegistrationRequest, StartOAuthLoginResponse, StepUpMethod as StepUpMethodShape, @@ -121,6 +122,13 @@ export type OAuthProvider = PublicOAuthProvider; export type OAuthProvidersResult = OAuthProvidersResponse; +/** + * The configuration a signed-out client may read. Fetched before there is a + * session, so the sign-in screens can match how this instance is configured + * instead of assuming a default set of methods. + */ +export type PublicSystemConfigResult = PublicSystemConfigResponse; + export interface StartOAuthLoginInput { providerId: string; redirectUri?: string; @@ -220,6 +228,7 @@ export interface SeamlessAuthClient { checkMagicLink: () => Promise>; verifyMagicLink: (token: string) => Promise>; listOAuthProviders: () => Promise>; + getPublicSystemConfig: () => Promise>; startOAuthLogin: ( input: StartOAuthLoginInput ) => Promise>; @@ -575,6 +584,12 @@ export const createSeamlessAuthClient = ( 'Failed to list OAuth providers.' ), + getPublicSystemConfig: () => + requestResult( + fetchWithAuth(`/system-config/public`, { method: 'GET' }), + 'Failed to read the public system configuration.' + ), + startOAuthLogin: input => requestResult( fetchWithAuth(`/oauth/${encodeURIComponent(input.providerId)}/start`, { diff --git a/src/components/AuthFallbackOptions.tsx b/src/components/AuthFallbackOptions.tsx index fb7e24b..f103029 100644 --- a/src/components/AuthFallbackOptions.tsx +++ b/src/components/AuthFallbackOptions.tsx @@ -16,7 +16,7 @@ interface AuthFallbackOptionsProps { onEmailOtp?: () => void; onPhoneOtp: () => void; onPasskeyRetry?: () => void; - loginMethods?: LoginMethod[]; + loginMethods?: LoginMethod[] | null; } const AuthFallbackOptions: React.FC = ({ @@ -27,6 +27,9 @@ const AuthFallbackOptions: React.FC = ({ onPasskeyRetry, loginMethods, }) => { + // Null means the caller has not resolved the methods yet. This component is + // the last step of a fallback flow, so it stays permissive and lets the + // handlers it was given decide, rather than hiding an option a caller wired up. const allowedMethods = new Set( loginMethods ?? ['passkey', 'magic_link', 'phone_otp'] ); diff --git a/src/hooks/useLoginMethods.ts b/src/hooks/useLoginMethods.ts new file mode 100644 index 0000000..a57bfd0 --- /dev/null +++ b/src/hooks/useLoginMethods.ts @@ -0,0 +1,74 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { useEffect, useState } from 'react'; + +import type { LoginMethod } from '@/client/createSeamlessAuthClient'; +import { useAuthClient } from '@/hooks/useAuthClient'; + +/** + * Used only until the instance answers, and if it never does. + * + * Deliberately the narrowest useful set, and matching the auth server's own + * defaults. Offering a method that turns out to be disabled sends a user down a + * path that fails, which is worse than showing one option too few. + */ +export const FALLBACK_LOGIN_METHODS: LoginMethod[] = ['passkey', 'magic_link']; + +/** + * Which sign-in methods this instance has enabled, read from the auth server + * rather than assumed. + * + * `loginMethods` stays null until the answer arrives, and stays null if the + * request fails. A caller must treat that as "unknown" and not as "none": the + * screens use it to decide what is safe to offer, and guessing in either + * direction is worse than waiting. `loading` is what a caller renders against. + */ +export const useLoginMethods = () => { + const authClient = useAuthClient(); + const [loginMethods, setLoginMethods] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + + const read = async () => { + try { + const { data, error } = await authClient.getPublicSystemConfig(); + + if (active && !error && data?.loginMethods?.length) { + setLoginMethods(data.loginMethods); + } + } catch { + // Backstop only. The client reports request failures through `error`, + // not by throwing, and either way the methods stay unknown. Leaving + // `loading` true here would hang every screen that waits on it. + } finally { + if (active) { + setLoading(false); + } + } + }; + + void read(); + + return () => { + active = false; + }; + }, [authClient]); + + return { loginMethods, loading }; +}; + +/** + * Whether a user who declines a passkey would still have a way to sign in. + * + * Returns false while the methods are unknown, so a failed or in-flight request + * never produces a skip control that could strand someone in an account they + * cannot get back into. + */ +export const hasNonPasskeyLoginMethod = (loginMethods: LoginMethod[] | null) => + Boolean(loginMethods?.some(method => method !== 'passkey')); diff --git a/src/index.ts b/src/index.ts index 629e95c..5dc286f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { MessageResult, OAuthProvider, OAuthProvidersResult, + PublicSystemConfigResult, OrganizationMemberInput, OrganizationMemberUpdateInput, OrganizationMembersResult, @@ -57,6 +58,7 @@ import { PasskeyPrfResult, } from '@/client/webauthnPrf'; import { useAuthClient } from '@/hooks/useAuthClient'; +import { hasNonPasskeyLoginMethod, useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; import { hasScopedRole, roleGrantsAccess } from '@/scopedRoles'; import { Credential, Organization, OrganizationMembership, User } from '@/types'; @@ -69,12 +71,14 @@ export { extractPasskeyPrfResult, getOAuthErrorCode, getWebAuthnErrorDetail, + hasNonPasskeyLoginMethod, hasScopedRole, isPasskeyPrfSupported, roleGrantsAccess, SeamlessAuthError, useAuth, useAuthClient, + useLoginMethods, usePasskeySupport, }; export type { @@ -105,6 +109,7 @@ export type { PasskeyPrfInput, PasskeyPrfResult, PasskeyRegistrationData, + PublicSystemConfigResult, RegisterInput, RegisterPasskeyOptions, SeamlessAuthClient, diff --git a/src/styles/registerPasskey.module.css b/src/styles/registerPasskey.module.css index 9a352c5..869ce80 100644 --- a/src/styles/registerPasskey.module.css +++ b/src/styles/registerPasskey.module.css @@ -91,3 +91,19 @@ .error { color: #f87171; } + +.skip { + margin-top: 1.5rem; + width: 100%; + font-size: 0.875rem; + color: #60a5fa; + text-align: center; + background: none; + border: none; + cursor: pointer; +} + +.skip:disabled { + opacity: 0.6; + cursor: default; +} diff --git a/src/views/Login.tsx b/src/views/Login.tsx index c5ef567..425fdbe 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -7,6 +7,7 @@ import { useAuth } from '@/AuthProvider'; import React, { useEffect, useState } from 'react'; import { useAuthClient } from '@/hooks/useAuthClient'; +import { FALLBACK_LOGIN_METHODS, useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; import { useNavigate } from 'react-router-dom'; import { authRoutePaths } from '@/routes'; @@ -16,13 +17,12 @@ import AuthFallbackOptions from '@/components/AuthFallbackOptions'; import OAuthProviderButtons from '@/components/OAuthProviderButtons'; import type { LoginMethod } from '@/client/createSeamlessAuthClient'; -const DEFAULT_LOGIN_METHODS: LoginMethod[] = ['passkey', 'magic_link', 'phone_otp']; - const Login: React.FC = () => { const navigate = useNavigate(); const { hasSignedInBefore, login, handlePasskeyLogin } = useAuth(); const authClient = useAuthClient(); const { passkeySupported } = usePasskeySupport(); + const { loginMethods: configuredMethods } = useLoginMethods(); const [identifier, setIdentifier] = useState(''); const [email, setEmail] = useState(''); const [mode, setMode] = useState<'login' | 'register'>('register'); @@ -30,7 +30,7 @@ const Login: React.FC = () => { const [emailError, setEmailError] = useState(''); const [identifierError, setIdentifierError] = useState(''); const [showFallbackOptions, setShowFallbackOptions] = useState(false); - const [loginMethods, setLoginMethods] = useState(DEFAULT_LOGIN_METHODS); + const [loginMethods, setLoginMethods] = useState(null); useEffect(() => { if (hasSignedInBefore) { @@ -126,9 +126,12 @@ const Login: React.FC = () => { return; } + // The login response is per-user and authoritative when present. The + // instance configuration is the better fallback than a hardcoded list, + // because it at least reflects this deployment. const availableMethods = loginStart?.loginMethods?.length ? loginStart.loginMethods - : DEFAULT_LOGIN_METHODS; + : (configuredMethods ?? FALLBACK_LOGIN_METHODS); setLoginMethods(availableMethods); if (passkeySupported && availableMethods.includes('passkey')) { diff --git a/src/views/PassKeyRegistration.tsx b/src/views/PassKeyRegistration.tsx index 687c1ad..580abeb 100644 --- a/src/views/PassKeyRegistration.tsx +++ b/src/views/PassKeyRegistration.tsx @@ -8,6 +8,7 @@ import { useAuth } from '@/AuthProvider'; import { PasskeyMetadata } from '@/client/createSeamlessAuthClient'; import React, { useState } from 'react'; import { useAuthClient } from '@/hooks/useAuthClient'; +import { hasNonPasskeyLoginMethod, useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; import { useNavigate } from 'react-router-dom'; @@ -19,6 +20,7 @@ const PasskeyRegistration: React.FC = () => { const { refreshSession } = useAuth(); const authClient = useAuthClient(); const { passkeySupported, loading: passkeySupportLoading } = usePasskeySupport(); + const { loginMethods, loading: loginMethodsLoading } = useLoginMethods(); const navigate = useNavigate(); const [status, setStatus] = useState<'idle' | 'success' | 'error' | 'loading'>('idle'); @@ -31,6 +33,20 @@ const PasskeyRegistration: React.FC = () => { deviceInfo: string; } | null>(null); + // The session already exists by the time this screen renders: the OTP step + // that led here established it. A passkey is an addition to that session + // rather than what completes registration, which is what makes leaving + // without one a legitimate way to finish and not an escape hatch. + // + // Gated on another method being enabled. With passkey as the only one, a user + // who skipped would have no way back into the account they just made. + const canSkip = hasNonPasskeyLoginMethod(loginMethods); + + const finishWithoutPasskey = async () => { + await refreshSession(); + navigate('/'); + }; + const openDeviceModal = () => { const { platform, browser, deviceInfo } = parseUserAgent(); @@ -73,14 +89,32 @@ const PasskeyRegistration: React.FC = () => { <>
- {passkeySupportLoading || !passkeySupported ? ( + {passkeySupportLoading || loginMethodsLoading ? (
- - {passkeySupportLoading - ? 'Checking for Passkey Support...' - : 'Passkeys are not supported on this device.'} - + Checking for Passkey Support... +
+ ) : !passkeySupported ? ( + // This used to be the end of the road: a message and no control of + // any kind, on a screen the user could not leave. Whether there is a + // way forward depends on the instance, so say which case this is. +
+

Passkeys are not available here

+

+ {canSkip + ? 'This device does not support passkeys. You can continue without one and add a passkey later from a device that does.' + : 'This device does not support passkeys, and this application requires one to sign in. Try again from a device or browser that supports them.'} +

+ + {canSkip && ( + + )}
) : (
@@ -106,6 +140,17 @@ const PasskeyRegistration: React.FC = () => { {message}

)} + + {canSkip && ( + + )}
)}
diff --git a/tests/RegisterPassKey.test.tsx b/tests/RegisterPassKey.test.tsx index 3af1608..bf51cbd 100644 --- a/tests/RegisterPassKey.test.tsx +++ b/tests/RegisterPassKey.test.tsx @@ -7,6 +7,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import RegisterPasskey from '../src/views/PassKeyRegistration'; import { useAuthClient } from '@/hooks/useAuthClient'; +import { useLoginMethods } from '@/hooks/useLoginMethods'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; const mockNavigate = jest.fn(); @@ -27,6 +28,13 @@ jest.mock('@/AuthProvider', () => ({ jest.mock('@/hooks/useAuthClient'); jest.mock('@/hooks/usePasskeySupport'); +// hasNonPasskeyLoginMethod stays real: it encodes the rule that decides whether +// a skip is safe to offer, so mocking it would test nothing. +jest.mock('@/hooks/useLoginMethods', () => ({ + ...jest.requireActual('@/hooks/useLoginMethods'), + useLoginMethods: jest.fn(), +})); + jest.mock('@/utils', () => ({ parseUserAgent: jest.fn().mockReturnValue({ platform: 'macOS', @@ -55,6 +63,10 @@ beforeEach(() => { passkeySupported: true, loading: false, }); + (useLoginMethods as jest.Mock).mockReturnValue({ + loginMethods: ['passkey', 'magic_link'], + loading: false, + }); }); describe('RegisterPasskey', () => { @@ -161,8 +173,83 @@ describe('RegisterPasskey', () => { render(); + expect(screen.getByText(/Passkeys are not available here/i)).toBeInTheDocument(); expect( - screen.getByText(/passkeys are not supported on this device/i) + screen.getByText(/This device does not support passkeys/i) ).toBeInTheDocument(); }); }); + +describe('RegisterPasskey skip control', () => { + it('offers a skip when another login method is enabled', async () => { + render(); + + fireEvent.click(await screen.findByText(/Skip for now/i)); + + await waitFor(() => { + expect(mockRefreshSession).toHaveBeenCalled(); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/'); + expect(mockRegisterPasskey).not.toHaveBeenCalled(); + }); + + // Skipping here would leave the user with no way back into the account they + // just created, so the control must not exist at all. + it('offers no skip when passkey is the only login method', async () => { + (useLoginMethods as jest.Mock).mockReturnValue({ + loginMethods: ['passkey'], + loading: false, + }); + + render(); + + await screen.findByText(/Secure Your Account/i); + expect(screen.queryByText(/Skip for now/i)).not.toBeInTheDocument(); + }); + + it('offers no skip while the login methods are still unknown', async () => { + (useLoginMethods as jest.Mock).mockReturnValue({ + loginMethods: null, + loading: false, + }); + + render(); + + await screen.findByText(/Secure Your Account/i); + expect(screen.queryByText(/Skip for now/i)).not.toBeInTheDocument(); + }); + + it('gives an unsupported device a way forward when another method is enabled', async () => { + (usePasskeySupport as jest.Mock).mockReturnValue({ + passkeySupported: false, + loading: false, + }); + + render(); + + fireEvent.click(await screen.findByText(/^Continue$/i)); + + await waitFor(() => { + expect(mockRefreshSession).toHaveBeenCalled(); + }); + + expect(mockNavigate).toHaveBeenCalledWith('/'); + }); + + it('tells an unsupported device it is stuck when passkey is the only method', async () => { + (usePasskeySupport as jest.Mock).mockReturnValue({ + passkeySupported: false, + loading: false, + }); + (useLoginMethods as jest.Mock).mockReturnValue({ + loginMethods: ['passkey'], + loading: false, + }); + + render(); + + expect(await screen.findByText(/requires one to sign in/i)).toBeInTheDocument(); + expect(screen.queryByText(/^Continue$/i)).not.toBeInTheDocument(); + }); +}); diff --git a/tests/login.test.tsx b/tests/login.test.tsx index f53f532..9b12e2f 100644 --- a/tests/login.test.tsx +++ b/tests/login.test.tsx @@ -45,6 +45,7 @@ describe('Login', () => { requestPhoneOtp: jest.fn(), requestLoginPhoneOtp: jest.fn(), requestLoginEmailOtp: jest.fn(), + getPublicSystemConfig: jest.fn(), }; beforeEach(() => { @@ -64,6 +65,11 @@ describe('Login', () => { loading: false, }); + mockAuthClient.getPublicSystemConfig.mockResolvedValue({ + data: { loginMethods: ['passkey', 'magic_link', 'phone_otp'] }, + error: null, + }); + (isValidEmail as jest.Mock).mockReturnValue(true); (isValidPhoneNumber as jest.Mock).mockReturnValue(false); diff --git a/tests/useLoginMethods.test.tsx b/tests/useLoginMethods.test.tsx new file mode 100644 index 0000000..01ee20d --- /dev/null +++ b/tests/useLoginMethods.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + * See LICENSE file in the project root for full license information + */ + +import { renderHook, waitFor } from '@testing-library/react'; + +import { useAuthClient } from '@/hooks/useAuthClient'; +import { hasNonPasskeyLoginMethod, useLoginMethods } from '@/hooks/useLoginMethods'; + +const mockGetPublicSystemConfig = jest.fn(); + +jest.mock('@/hooks/useAuthClient'); + +beforeEach(() => { + jest.clearAllMocks(); + (useAuthClient as jest.Mock).mockReturnValue({ + getPublicSystemConfig: mockGetPublicSystemConfig, + }); +}); + +describe('useLoginMethods', () => { + it('returns the methods the instance reports', async () => { + mockGetPublicSystemConfig.mockResolvedValue({ + data: { loginMethods: ['passkey', 'phone_otp'] }, + error: null, + }); + + const { result } = renderHook(() => useLoginMethods()); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.loginMethods).toEqual(['passkey', 'phone_otp']); + }); + + // Null is "unknown", never "none". A caller that treated a failed read as an + // empty list would draw conclusions the server never sent. + it('leaves the methods unknown when the read fails', async () => { + mockGetPublicSystemConfig.mockResolvedValue({ + data: null, + error: new Error('network'), + }); + + const { result } = renderHook(() => useLoginMethods()); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.loginMethods).toBeNull(); + }); + + it('leaves the methods unknown when the instance reports an empty list', async () => { + mockGetPublicSystemConfig.mockResolvedValue({ + data: { loginMethods: [] }, + error: null, + }); + + const { result } = renderHook(() => useLoginMethods()); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.loginMethods).toBeNull(); + }); +}); + +describe('hasNonPasskeyLoginMethod', () => { + it.each([ + ['another method is enabled', ['passkey', 'magic_link'], true], + ['passkey is the only method', ['passkey'], false], + ['the methods are unknown', null, false], + ['the list is empty', [], false], + ])('is %s', (_label, methods, expected) => { + expect(hasNonPasskeyLoginMethod(methods as never)).toBe(expected); + }); +}); From ae3a0daed027c21198e8e92f94c62918811fbb82 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 30 Jul 2026 22:53:32 -0400 Subject: [PATCH 2/2] chore(deps): move onto @seamless-auth/types 0.5.0 The release carrying PublicSystemConfigResponse, which the public system config client method and useLoginMethods are typed against. Verified against the published package with npm run typecheck, npm test (290 passing), npm run lint, npm run format:check, and npm run build. --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1738812..6533824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@seamless-auth/react", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamless-auth/react", - "version": "0.6.0", + "version": "0.7.0", "license": "AGPL-3.0-only", "dependencies": { - "@seamless-auth/types": "^0.4.0", + "@seamless-auth/types": "^0.5.0", "@simplewebauthn/browser": "^13.1.0", "eslint-plugin-license-header": "^0.9.0", "libphonenumber-js": "^1.12.7", @@ -3066,9 +3066,9 @@ "license": "MIT" }, "node_modules/@seamless-auth/types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.4.0.tgz", - "integrity": "sha512-1WhLjgyCN9UdX6TEzKbbASROeLMxZmo2LiAYOlYE2sGIPojnbAE/yFbp5Ydt13GibiZ6MwYlHMLcsBVQMeelWA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.5.0.tgz", + "integrity": "sha512-T566YQu8FqUmqgte0A0iJpjTDXUKq/d/7uvLlXGXKtdl52Edo0r3iafL3kx4OM0cRJM63tBGe9s4MTjRUth6yA==", "license": "AGPL-3.0-only", "dependencies": { "zod": "^4.3.6" diff --git a/package.json b/package.json index 49ceaa2..d757737 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "typescript-eslint": "^8.46.1" }, "dependencies": { - "@seamless-auth/types": "^0.4.0", + "@seamless-auth/types": "^0.5.0", "@simplewebauthn/browser": "^13.1.0", "eslint-plugin-license-header": "^0.9.0", "libphonenumber-js": "^1.12.7",