@seamless-auth/svelte 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +202 -0
  3. package/README.md +253 -2
  4. package/dist/auth.svelte.d.ts +170 -0
  5. package/dist/auth.svelte.js +208 -0
  6. package/dist/config.d.ts +49 -0
  7. package/dist/config.js +6 -0
  8. package/dist/context.d.ts +13 -0
  9. package/dist/context.js +35 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.js +21 -0
  12. package/dist/kit/index.d.ts +55 -0
  13. package/dist/kit/index.js +121 -0
  14. package/dist/kit/watch.svelte.d.ts +10 -0
  15. package/dist/kit/watch.svelte.js +28 -0
  16. package/dist/navigation.d.ts +40 -0
  17. package/dist/navigation.js +22 -0
  18. package/dist/screens/AuthLayout.svelte +38 -0
  19. package/dist/screens/AuthLayout.svelte.d.ts +8 -0
  20. package/dist/screens/FallbackOptions.svelte +69 -0
  21. package/dist/screens/FallbackOptions.svelte.d.ts +13 -0
  22. package/dist/screens/Login.svelte +253 -0
  23. package/dist/screens/Login.svelte.d.ts +3 -0
  24. package/dist/screens/MagicLinkSent.svelte +74 -0
  25. package/dist/screens/MagicLinkSent.svelte.d.ts +3 -0
  26. package/dist/screens/OAuthCallback.svelte +69 -0
  27. package/dist/screens/OAuthCallback.svelte.d.ts +3 -0
  28. package/dist/screens/OAuthProviderButtons.svelte +49 -0
  29. package/dist/screens/OAuthProviderButtons.svelte.d.ts +6 -0
  30. package/dist/screens/OtpInput.svelte +101 -0
  31. package/dist/screens/OtpInput.svelte.d.ts +10 -0
  32. package/dist/screens/PasskeyLogin.svelte +34 -0
  33. package/dist/screens/PasskeyLogin.svelte.d.ts +3 -0
  34. package/dist/screens/RegisterPasskey.svelte +110 -0
  35. package/dist/screens/RegisterPasskey.svelte.d.ts +3 -0
  36. package/dist/screens/VerifyMagicLink.svelte +104 -0
  37. package/dist/screens/VerifyMagicLink.svelte.d.ts +3 -0
  38. package/dist/screens/VerifyOtp.svelte +141 -0
  39. package/dist/screens/VerifyOtp.svelte.d.ts +7 -0
  40. package/dist/seamless-auth.css +260 -0
  41. package/dist/styles.generated.d.ts +2 -0
  42. package/dist/styles.generated.js +3 -0
  43. package/package.json +76 -4
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { createAuthSession, createBrowserOAuthRedirect, createBrowserPasskeyPort, createMemoryStorage, detectPasskeySupport, loadLoginMethods, } from '@seamless-auth/client';
7
+ const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
8
+ /**
9
+ * Session state and auth actions for a Svelte application, from
10
+ * `createSeamlessAuth`.
11
+ *
12
+ * A thin binding over the framework-agnostic session store in
13
+ * `@seamless-auth/client`: the store owns the state machine, and this mirrors
14
+ * it into runes, so every property reads reactively in a component or an
15
+ * `$effect`. Change the session through the actions.
16
+ */
17
+ export class SeamlessAuth {
18
+ #session;
19
+ // Assigned in the constructor, once the session exists.
20
+ #state = $state.raw(undefined);
21
+ #loginMethods = $state.raw(null);
22
+ #loginMethodsLoading = $state(true);
23
+ #passkeySupported = $state(false);
24
+ #passkeySupportLoading = $state(true);
25
+ #loginMethodsRequest = null;
26
+ #passkeySupportRequest = null;
27
+ #pendingSettles = new Set();
28
+ #unsubscribe;
29
+ #browser;
30
+ /** The platform ports the bundled screens go through. */
31
+ ports;
32
+ /** The client behind the session, for calls the session does not wrap. */
33
+ client;
34
+ /** How the bundled screens add their stylesheet. */
35
+ styles;
36
+ /** Where the bundled screens go once someone is signed in. */
37
+ signedInPath;
38
+ /** Where `requireAuth` sends someone who is signed out. */
39
+ loginPath;
40
+ constructor(config) {
41
+ this.#browser = isBrowser();
42
+ this.ports = {
43
+ passkeys: config.ports?.passkeys ?? createBrowserPasskeyPort(),
44
+ oauthRedirect: config.ports?.oauthRedirect ?? createBrowserOAuthRedirect(),
45
+ };
46
+ this.#session = createAuthSession({
47
+ apiHost: config.apiHost,
48
+ magicLinkRedirectUri: config.magicLinkRedirectUri,
49
+ // Cookie transport only. `mode` is deliberately not configurable here, so
50
+ // no token is ever handed to page scripts.
51
+ transport: {
52
+ basePath: config.basePath,
53
+ fetch: config.fetch,
54
+ trustedOrigins: config.trustedOrigins,
55
+ },
56
+ passkeys: this.ports.passkeys,
57
+ detectPreviousSignIn: config.autoDetectPreviousSignIn ?? true,
58
+ initialSession: config.initialSession,
59
+ // A server render has no browser storage to read.
60
+ storage: this.#browser ? undefined : createMemoryStorage(),
61
+ });
62
+ this.client = this.#session.client;
63
+ this.styles = { inject: config.injectStyles ?? true, nonce: config.cspNonce };
64
+ this.signedInPath = config.signedInPath ?? '/';
65
+ this.loginPath = config.loginPath ?? '/login';
66
+ // A server render shows what the server knows; the browser reads the rest
67
+ // as soon as it runs.
68
+ const read = this.#browser ? this.#session.getState : this.#session.getServerState;
69
+ this.#state = read();
70
+ this.#unsubscribe = this.#session.subscribe(() => {
71
+ this.#state = read();
72
+ });
73
+ if (this.#browser) {
74
+ void this.#session.actions.refreshSession({
75
+ background: config.initialSession !== undefined,
76
+ });
77
+ }
78
+ }
79
+ /** Everything the session knows. Replaced wholesale on every change. */
80
+ get state() {
81
+ return this.#state;
82
+ }
83
+ get user() {
84
+ return this.#state.user;
85
+ }
86
+ get isAuthenticated() {
87
+ return this.#state.isAuthenticated;
88
+ }
89
+ /** True until the session has been read. Guards wait on this. */
90
+ get loading() {
91
+ return this.#state.loading;
92
+ }
93
+ get credentials() {
94
+ return this.#state.credentials;
95
+ }
96
+ get organizations() {
97
+ return this.#state.organizations;
98
+ }
99
+ get activeOrganization() {
100
+ return this.#state.activeOrganization;
101
+ }
102
+ get stepUpStatus() {
103
+ return this.#state.stepUpStatus;
104
+ }
105
+ get hasSignedInBefore() {
106
+ return this.#state.hasSignedInBefore;
107
+ }
108
+ /**
109
+ * Sign-in methods this instance has enabled, once `loadLoginMethods()` has
110
+ * answered. Null means unknown, never "none".
111
+ */
112
+ get loginMethods() {
113
+ return this.#loginMethods;
114
+ }
115
+ get loginMethodsLoading() {
116
+ return this.#loginMethodsLoading;
117
+ }
118
+ /** Whether this device can use a passkey, once `checkPasskeySupport()` has answered. */
119
+ get passkeySupported() {
120
+ return this.#passkeySupported;
121
+ }
122
+ get passkeySupportLoading() {
123
+ return this.#passkeySupportLoading;
124
+ }
125
+ /** Reads the instance's sign-in methods once and caches the answer. */
126
+ loadLoginMethods() {
127
+ this.#loginMethodsRequest ??= loadLoginMethods(this.client).then(methods => {
128
+ this.#loginMethods = methods;
129
+ this.#loginMethodsLoading = false;
130
+ return methods;
131
+ });
132
+ return this.#loginMethodsRequest;
133
+ }
134
+ /** Detects passkey support once and caches the answer. */
135
+ checkPasskeySupport() {
136
+ this.#passkeySupportRequest ??= (this.#browser ? detectPasskeySupport(this.ports.passkeys) : Promise.resolve(false)).then(supported => {
137
+ this.#passkeySupported = supported;
138
+ this.#passkeySupportLoading = false;
139
+ return supported;
140
+ });
141
+ return this.#passkeySupportRequest;
142
+ }
143
+ /**
144
+ * Resolves with the state once the session has been read. During a server
145
+ * render the session is never read there, so this resolves at once with what
146
+ * the server knows. It also resolves if the session is destroyed first.
147
+ */
148
+ whenSettled() {
149
+ const current = this.#state;
150
+ if (!current.loading || !this.#browser) {
151
+ return Promise.resolve(current);
152
+ }
153
+ return new Promise(resolve => {
154
+ const settle = (next) => {
155
+ stop();
156
+ this.#pendingSettles.delete(abandon);
157
+ resolve(next);
158
+ };
159
+ const abandon = () => settle(this.#state);
160
+ const stop = this.#session.subscribe(() => {
161
+ const next = this.#session.getState();
162
+ if (!next.loading)
163
+ settle(next);
164
+ });
165
+ this.#pendingSettles.add(abandon);
166
+ });
167
+ }
168
+ /**
169
+ * A fetch for the application's own API that carries the session cookies.
170
+ * A path resolves on `apiHost`. Only `apiHost` and `trustedOrigins` receive
171
+ * the session; any other origin rejects with `UntrustedOriginError`.
172
+ */
173
+ authorizedFetch = (input, init) => this.client.authorizedFetch(input, init);
174
+ login = (...args) => this.#session.actions.login(...args);
175
+ handlePasskeyLogin = () => this.#session.actions.handlePasskeyLogin();
176
+ registerPasskey = input => this.#session.actions.registerPasskey(input);
177
+ refreshSession = options => this.#session.actions.refreshSession(options);
178
+ logout = () => this.#session.actions.logout();
179
+ logoutAllSessions = () => this.#session.actions.logoutAllSessions();
180
+ deleteUser = () => this.#session.actions.deleteUser();
181
+ updateCredential = credential => this.#session.actions.updateCredential(credential);
182
+ deleteCredential = id => this.#session.actions.deleteCredential(id);
183
+ switchOrganization = id => this.#session.actions.switchOrganization(id);
184
+ listOAuthProviders = () => this.#session.actions.listOAuthProviders();
185
+ startOAuthLogin = input => this.#session.actions.startOAuthLogin(input);
186
+ finishOAuthLogin = input => this.#session.actions.finishOAuthLogin(input);
187
+ refreshStepUpStatus = () => this.#session.actions.refreshStepUpStatus();
188
+ verifyStepUpWithPasskey = () => this.#session.actions.verifyStepUpWithPasskey();
189
+ verifyStepUpWithPasskeyPrf = input => this.#session.actions.verifyStepUpWithPasskeyPrf(input);
190
+ verifyStepUpWithTotp = code => this.#session.actions.verifyStepUpWithTotp(code);
191
+ hasRole = role => this.#session.actions.hasRole(role);
192
+ hasScopedRole = role => this.#session.actions.hasScopedRole(role);
193
+ markSignedIn = () => this.#session.actions.markSignedIn();
194
+ /** Stops the session. Pending `whenSettled()` calls resolve with the last state. */
195
+ destroy() {
196
+ [...this.#pendingSettles].forEach(abandon => abandon());
197
+ this.#unsubscribe();
198
+ this.#session.destroy();
199
+ }
200
+ }
201
+ /**
202
+ * Creates the session for an application. In the browser it is read straight
203
+ * away. Create one per application (one per request when rendering on a
204
+ * server), and hand it to the screens with `setSeamlessAuth`.
205
+ */
206
+ export function createSeamlessAuth(config) {
207
+ return new SeamlessAuth(config);
208
+ }
@@ -0,0 +1,49 @@
1
+ import type { InitialSession, OAuthRedirectPort, PasskeyPort } from '@seamless-auth/client';
2
+ /**
3
+ * What a browser application configures. There is no transport mode: a Svelte
4
+ * application runs in the browser, where the server adapter holds the tokens
5
+ * and the browser holds only its HttpOnly cookies.
6
+ */
7
+ export interface SeamlessAuthConfig {
8
+ /** Origin of the server adapter, for example `https://app.example.com`. */
9
+ apiHost: string;
10
+ /** Where the adapter is mounted on `apiHost`. Defaults to `/auth`. */
11
+ basePath?: string;
12
+ /** Where a magic link sent by the bundled screens should land. */
13
+ magicLinkRedirectUri?: string;
14
+ /**
15
+ * Whether `hasSignedInBefore` reports a previous sign-in from this browser,
16
+ * which the bundled login screen uses to open on Sign In rather than Create
17
+ * Account. Defaults to true.
18
+ */
19
+ autoDetectPreviousSignIn?: boolean;
20
+ /**
21
+ * The session a server already resolved for this request, or `null` when it
22
+ * found none. The first render is then settled rather than loading.
23
+ */
24
+ initialSession?: InitialSession | null;
25
+ /** Platform ports. Each one left out falls back to the browser's. */
26
+ ports?: Partial<SeamlessAuthPorts>;
27
+ /** A fetch to send auth requests with. Defaults to the global one. */
28
+ fetch?: typeof fetch;
29
+ /**
30
+ * Origins besides `apiHost` that `authorizedFetch` may send the session
31
+ * cookies to. Every other origin is refused.
32
+ */
33
+ trustedOrigins?: string[];
34
+ /**
35
+ * Whether the bundled screens add their stylesheet to the page. Defaults to
36
+ * true. Set false to link `@seamless-auth/svelte/seamless-auth.css` yourself.
37
+ */
38
+ injectStyles?: boolean;
39
+ /** A CSP nonce for the stylesheet the screens add, when `injectStyles` is on. */
40
+ cspNonce?: string;
41
+ /** Where the bundled screens go once someone is signed in. Defaults to `/`. */
42
+ signedInPath?: string;
43
+ /** Where `requireAuth` sends someone who is signed out. Defaults to `/login`. */
44
+ loginPath?: string;
45
+ }
46
+ export interface SeamlessAuthPorts {
47
+ passkeys: PasskeyPort;
48
+ oauthRedirect: OAuthRedirectPort;
49
+ }
package/dist/config.js ADDED
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { SeamlessAuth } from './auth.svelte.js';
2
+ import type { AuthNavigator } from './navigation.js';
3
+ /**
4
+ * Makes the session available to the components below, the bundled screens
5
+ * included. Call it while a root component (a SvelteKit `+layout.svelte`, or
6
+ * `App.svelte`) initialises.
7
+ */
8
+ export declare function setSeamlessAuth(auth: SeamlessAuth): SeamlessAuth;
9
+ /** The session set by `setSeamlessAuth`. Call it while a component initialises. */
10
+ export declare function getSeamlessAuth(): SeamlessAuth;
11
+ /** Tells the bundled screens how to navigate. See `AuthNavigator`. */
12
+ export declare function setAuthNavigator(navigator: AuthNavigator): AuthNavigator;
13
+ export declare function getAuthNavigator(): AuthNavigator;
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { getContext, setContext } from 'svelte';
7
+ const AUTH_KEY = Symbol('seamless-auth');
8
+ const NAVIGATOR_KEY = Symbol('seamless-auth-navigator');
9
+ /**
10
+ * Makes the session available to the components below, the bundled screens
11
+ * included. Call it while a root component (a SvelteKit `+layout.svelte`, or
12
+ * `App.svelte`) initialises.
13
+ */
14
+ export function setSeamlessAuth(auth) {
15
+ return setContext(AUTH_KEY, auth);
16
+ }
17
+ /** The session set by `setSeamlessAuth`. Call it while a component initialises. */
18
+ export function getSeamlessAuth() {
19
+ const auth = getContext(AUTH_KEY);
20
+ if (!auth) {
21
+ throw new Error('Seamless Auth is not set. Call setSeamlessAuth(createSeamlessAuth({ apiHost })) in a root component.');
22
+ }
23
+ return auth;
24
+ }
25
+ /** Tells the bundled screens how to navigate. See `AuthNavigator`. */
26
+ export function setAuthNavigator(navigator) {
27
+ return setContext(NAVIGATOR_KEY, navigator);
28
+ }
29
+ export function getAuthNavigator() {
30
+ const navigator = getContext(NAVIGATOR_KEY);
31
+ if (!navigator) {
32
+ throw new Error('The Seamless Auth screens need a navigator. In SvelteKit, call setAuthNavigator(createKitNavigator(auth)) in your root layout.');
33
+ }
34
+ return navigator;
35
+ }
@@ -0,0 +1,18 @@
1
+ export { createSeamlessAuth, SeamlessAuth } from './auth.svelte.js';
2
+ export type { SeamlessAuthConfig, SeamlessAuthPorts } from './config.js';
3
+ export { getAuthNavigator, getSeamlessAuth, setAuthNavigator, setSeamlessAuth, } from './context.js';
4
+ export { authRoutePaths } from './navigation.js';
5
+ export type { AuthNavigator, AuthScreen } from './navigation.js';
6
+ export { default as SaAuthLayout } from './screens/AuthLayout.svelte';
7
+ export { default as SaFallbackOptions } from './screens/FallbackOptions.svelte';
8
+ export { default as SaLogin } from './screens/Login.svelte';
9
+ export { default as SaMagicLinkSent } from './screens/MagicLinkSent.svelte';
10
+ export { default as SaOAuthCallback } from './screens/OAuthCallback.svelte';
11
+ export { default as SaOAuthProviderButtons } from './screens/OAuthProviderButtons.svelte';
12
+ export { default as SaOtpInput } from './screens/OtpInput.svelte';
13
+ export { default as SaPasskeyLogin } from './screens/PasskeyLogin.svelte';
14
+ export { default as SaRegisterPasskey } from './screens/RegisterPasskey.svelte';
15
+ export { default as SaVerifyMagicLink } from './screens/VerifyMagicLink.svelte';
16
+ export { default as SaVerifyOtp } from './screens/VerifyOtp.svelte';
17
+ export { getOAuthErrorCode, getPasskeyPolicyErrorCode, getWebAuthnErrorDetail, hasScopedRole, isUnauthenticated, roleGrantsAccess, SeamlessAuthError, UntrustedOriginError, } from '@seamless-auth/client';
18
+ export type { AuthSessionState, Credential, CurrentUserResult, InitialSession, LoginMethod, OAuthProvider, OAuthRedirectPort, Organization, PasskeyPort, RefreshSessionOptions, SeamlessAuthClient, SeamlessAuthResult, StepUpStatus, User, } from '@seamless-auth/client';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ export { createSeamlessAuth, SeamlessAuth } from './auth.svelte.js';
7
+ export { getAuthNavigator, getSeamlessAuth, setAuthNavigator, setSeamlessAuth, } from './context.js';
8
+ export { authRoutePaths } from './navigation.js';
9
+ export { default as SaAuthLayout } from './screens/AuthLayout.svelte';
10
+ export { default as SaFallbackOptions } from './screens/FallbackOptions.svelte';
11
+ export { default as SaLogin } from './screens/Login.svelte';
12
+ export { default as SaMagicLinkSent } from './screens/MagicLinkSent.svelte';
13
+ export { default as SaOAuthCallback } from './screens/OAuthCallback.svelte';
14
+ export { default as SaOAuthProviderButtons } from './screens/OAuthProviderButtons.svelte';
15
+ export { default as SaOtpInput } from './screens/OtpInput.svelte';
16
+ export { default as SaPasskeyLogin } from './screens/PasskeyLogin.svelte';
17
+ export { default as SaRegisterPasskey } from './screens/RegisterPasskey.svelte';
18
+ export { default as SaVerifyMagicLink } from './screens/VerifyMagicLink.svelte';
19
+ export { default as SaVerifyOtp } from './screens/VerifyOtp.svelte';
20
+ // The client core, re-exported so an application imports from one package.
21
+ export { getOAuthErrorCode, getPasskeyPolicyErrorCode, getWebAuthnErrorDetail, hasScopedRole, isUnauthenticated, roleGrantsAccess, SeamlessAuthError, UntrustedOriginError, } from '@seamless-auth/client';
@@ -0,0 +1,55 @@
1
+ import type { SeamlessAuth } from '../auth.svelte.js';
2
+ import { type AuthNavigator, type AuthScreen } from '../navigation.js';
3
+ /** The parts of a SvelteKit load event the guards use, in both Kit 2 and 3. */
4
+ export interface GuardLoadEvent {
5
+ url: URL;
6
+ depends(...deps: `${string}:${string}`[]): void;
7
+ }
8
+ export interface KitNavigatorOptions {
9
+ /** Where each bundled screen is mounted, when not at its default path. */
10
+ paths?: Partial<Record<AuthScreen, string>>;
11
+ }
12
+ /**
13
+ * The bundled screens' navigator for SvelteKit, from `$app/navigation`. Create
14
+ * it while the root layout initialises, so it can also re-run the `load` guards
15
+ * whenever the session changes:
16
+ *
17
+ * ```svelte
18
+ * setSeamlessAuth(auth);
19
+ * setAuthNavigator(createKitNavigator(auth));
20
+ * ```
21
+ */
22
+ export declare function createKitNavigator(auth: SeamlessAuth, options?: KitNavigatorOptions): AuthNavigator;
23
+ export interface RequireAuthOptions {
24
+ /** Where to send someone who is signed out. Defaults to the configured `loginPath`. */
25
+ redirectTo?: string;
26
+ /**
27
+ * Roles the user needs, matched the way the auth API matches them (scoped
28
+ * roles such as `org:admin` included). Any one of a list is enough.
29
+ */
30
+ roles?: string | string[];
31
+ /** Where to send a signed-in user without the role. Defaults to `signedInPath`. */
32
+ forbiddenRedirectTo?: string;
33
+ }
34
+ /**
35
+ * A `load` function that admits a signed-in user, for a `+layout.ts` or
36
+ * `+page.ts`:
37
+ *
38
+ * ```ts
39
+ * export const load = requireAuth(auth);
40
+ * ```
41
+ *
42
+ * It waits for the session to be read, so reloading a protected page does not
43
+ * bounce a signed-in user, and runs again whenever the session changes. A
44
+ * server render that was not handed an `initialSession` cannot see the session
45
+ * and treats the visitor as signed out, so set `export const ssr = false` on
46
+ * protected routes, which suits a backend-for-frontend application anyway.
47
+ */
48
+ export declare function requireAuth(auth: SeamlessAuth, options?: RequireAuthOptions): (event: GuardLoadEvent) => Promise<void>;
49
+ /**
50
+ * A `load` function that admits someone who is signed out, and sends a signed-in
51
+ * user to `signedInPath`. For the screens that start a sign-in.
52
+ */
53
+ export declare function requireGuest(auth: SeamlessAuth, options?: {
54
+ redirectTo?: string;
55
+ }): (event: GuardLoadEvent) => Promise<void>;
@@ -0,0 +1,121 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { redirect } from '@sveltejs/kit';
7
+ import { afterNavigate, goto } from '$app/navigation';
8
+ import { resolve as resolveRoute } from '$app/paths';
9
+ import { page } from '$app/state';
10
+ import { authRoutePaths } from '../navigation.js';
11
+ import { invalidateOnSessionChange, SESSION_DEPENDENCY } from './watch.svelte.js';
12
+ // Kit types `resolve` against the application's own route ids, which a library
13
+ // cannot know, so it is called as the plain pathname resolver it also is.
14
+ const resolve = resolveRoute;
15
+ /**
16
+ * The bundled screens' navigator for SvelteKit, from `$app/navigation`. Create
17
+ * it while the root layout initialises, so it can also re-run the `load` guards
18
+ * whenever the session changes:
19
+ *
20
+ * ```svelte
21
+ * setSeamlessAuth(auth);
22
+ * setAuthNavigator(createKitNavigator(auth));
23
+ * ```
24
+ */
25
+ export function createKitNavigator(auth, options = {}) {
26
+ const pathOf = (screen) => resolve(options.paths?.[screen] ?? authRoutePaths[screen]);
27
+ const base = resolve('/').replace(/\/+$/, '');
28
+ // A screen mounts while Kit is still hydrating the first page, before its
29
+ // router has started, and a navigation then fails. The first afterNavigate
30
+ // marks the end of hydration.
31
+ let markReady = () => undefined;
32
+ const ready = new Promise(resolveReady => {
33
+ markReady = resolveReady;
34
+ });
35
+ try {
36
+ afterNavigate(() => markReady());
37
+ invalidateOnSessionChange(auth);
38
+ }
39
+ catch {
40
+ // Created outside a component, so there is no hydration to wait for.
41
+ markReady();
42
+ }
43
+ const toApp = (path) => goto(resolve(path ?? auth.signedInPath));
44
+ return {
45
+ toScreen: (screen, state) => goto(pathOf(screen), { state: state ?? {} }),
46
+ toApp,
47
+ // A browser path already carries the base. One outside it (the '/' an OAuth
48
+ // sign-in falls back to, under a base path) is not in this application, so
49
+ // it goes to signedInPath instead.
50
+ toLocation: async (browserPath) => {
51
+ if (base && browserPath !== base && !browserPath.startsWith(`${base}/`)) {
52
+ return toApp();
53
+ }
54
+ try {
55
+ await goto(browserPath);
56
+ }
57
+ catch {
58
+ await toApp();
59
+ }
60
+ },
61
+ state: () => (page.state ?? {}),
62
+ query: name => page.url.searchParams.get(name),
63
+ // A real replace navigation to the same screen, which stays mounted. Shallow
64
+ // routing would only change the address bar: Kit keeps the old URL in
65
+ // page.url and in the history entry, where Back would bring the secret back.
66
+ dropQuery: async () => {
67
+ await ready;
68
+ // afterNavigate runs just before Kit marks its router started.
69
+ await Promise.resolve();
70
+ await goto(page.url.pathname, { replaceState: true, state: page.state });
71
+ },
72
+ absoluteUrl: screen => new URL(pathOf(screen), window.location.origin).toString(),
73
+ };
74
+ }
75
+ /**
76
+ * Makes Kit run a guard again on every navigation and whenever the session
77
+ * changes, instead of once per layout. Cheap: a settled session answers at once.
78
+ */
79
+ function track(event) {
80
+ event.depends(SESSION_DEPENDENCY);
81
+ void event.url.pathname;
82
+ }
83
+ /**
84
+ * A `load` function that admits a signed-in user, for a `+layout.ts` or
85
+ * `+page.ts`:
86
+ *
87
+ * ```ts
88
+ * export const load = requireAuth(auth);
89
+ * ```
90
+ *
91
+ * It waits for the session to be read, so reloading a protected page does not
92
+ * bounce a signed-in user, and runs again whenever the session changes. A
93
+ * server render that was not handed an `initialSession` cannot see the session
94
+ * and treats the visitor as signed out, so set `export const ssr = false` on
95
+ * protected routes, which suits a backend-for-frontend application anyway.
96
+ */
97
+ export function requireAuth(auth, options = {}) {
98
+ return async (event) => {
99
+ track(event);
100
+ const state = await auth.whenSettled();
101
+ if (state.loading || !state.isAuthenticated) {
102
+ redirect(307, resolve(options.redirectTo ?? auth.loginPath));
103
+ }
104
+ if (options.roles !== undefined && !auth.hasScopedRole(options.roles)) {
105
+ redirect(307, resolve(options.forbiddenRedirectTo ?? auth.signedInPath));
106
+ }
107
+ };
108
+ }
109
+ /**
110
+ * A `load` function that admits someone who is signed out, and sends a signed-in
111
+ * user to `signedInPath`. For the screens that start a sign-in.
112
+ */
113
+ export function requireGuest(auth, options = {}) {
114
+ return async (event) => {
115
+ track(event);
116
+ const state = await auth.whenSettled();
117
+ if (state.isAuthenticated) {
118
+ redirect(307, resolve(options.redirectTo ?? auth.signedInPath));
119
+ }
120
+ };
121
+ }
@@ -0,0 +1,10 @@
1
+ import type { SeamlessAuth } from '../auth.svelte.js';
2
+ /** What the guards register, so a change in the session runs them again. */
3
+ export declare const SESSION_DEPENDENCY = "seamless-auth:session";
4
+ /**
5
+ * Re-runs every `load` guard when who is signed in, or as which organization,
6
+ * changes: a sign-out in another tab, an expired session, a switched
7
+ * organization. Without it a layout's guard runs once and its child pages keep
8
+ * rendering. Call it while a component initialises.
9
+ */
10
+ export declare function invalidateOnSessionChange(auth: SeamlessAuth): void;
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ import { invalidate } from '$app/navigation';
7
+ /** What the guards register, so a change in the session runs them again. */
8
+ export const SESSION_DEPENDENCY = 'seamless-auth:session';
9
+ /**
10
+ * Re-runs every `load` guard when who is signed in, or as which organization,
11
+ * changes: a sign-out in another tab, an expired session, a switched
12
+ * organization. Without it a layout's guard runs once and its child pages keep
13
+ * rendering. Call it while a component initialises.
14
+ */
15
+ export function invalidateOnSessionChange(auth) {
16
+ let first = true;
17
+ $effect(() => {
18
+ // Read for tracking: these are what a guard decides on.
19
+ void auth.isAuthenticated;
20
+ void auth.user;
21
+ void auth.activeOrganization;
22
+ if (first) {
23
+ first = false;
24
+ return;
25
+ }
26
+ void invalidate(SESSION_DEPENDENCY);
27
+ });
28
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The bundled screens, by key, and their default paths.
3
+ *
4
+ * Two paths are set by contracts outside this package: `verifyMagicLink` is the
5
+ * URL the auth API builds when it emails a magic link, and `oauthCallback` is
6
+ * registered with OAuth providers as an allowed redirect URI.
7
+ */
8
+ export declare const authRoutePaths: {
9
+ readonly login: "/login";
10
+ readonly passkeyLogin: "/passkey-login";
11
+ readonly verifyPhoneOtp: "/verify-phone-otp";
12
+ readonly verifyEmailOtp: "/verify-email-otp";
13
+ readonly verifyMagicLink: "/verify-magiclink";
14
+ readonly oauthCallback: "/oauth/callback";
15
+ readonly registerPasskey: "/register-passkey";
16
+ readonly magicLinkSent: "/magic-link-sent";
17
+ };
18
+ export type AuthScreen = keyof typeof authRoutePaths;
19
+ /**
20
+ * How the bundled screens move, supplied by the application's router. The
21
+ * screens never import a router themselves, so they work in SvelteKit (see
22
+ * `createKitNavigator` in `@seamless-auth/svelte/kit`) or with any other router
23
+ * that can implement this.
24
+ */
25
+ export interface AuthNavigator {
26
+ /** Go to another bundled screen, handing it some navigation state. */
27
+ toScreen(screen: AuthScreen, state?: Record<string, string>): Promise<void>;
28
+ /** Go to an in-app path, or to the configured `signedInPath`. */
29
+ toApp(path?: string): Promise<void>;
30
+ /** Go to a path as the browser sees it, base path included. */
31
+ toLocation(browserPath: string): Promise<void>;
32
+ /** The navigation state the previous screen handed over. */
33
+ state(): Record<string, unknown>;
34
+ /** A query parameter of the current URL. */
35
+ query(name: string): string | null;
36
+ /** Drop the query from the current URL in place, without leaving the screen. */
37
+ dropQuery(): Promise<void>;
38
+ /** A screen's absolute URL, for a redirect URI. */
39
+ absoluteUrl(screen: AuthScreen): string;
40
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright © 2026 Fells Code, LLC
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file in the project root for full license information
5
+ */
6
+ /**
7
+ * The bundled screens, by key, and their default paths.
8
+ *
9
+ * Two paths are set by contracts outside this package: `verifyMagicLink` is the
10
+ * URL the auth API builds when it emails a magic link, and `oauthCallback` is
11
+ * registered with OAuth providers as an allowed redirect URI.
12
+ */
13
+ export const authRoutePaths = {
14
+ login: '/login',
15
+ passkeyLogin: '/passkey-login',
16
+ verifyPhoneOtp: '/verify-phone-otp',
17
+ verifyEmailOtp: '/verify-email-otp',
18
+ verifyMagicLink: '/verify-magiclink',
19
+ oauthCallback: '/oauth/callback',
20
+ registerPasskey: '/register-passkey',
21
+ magicLinkSent: '/magic-link-sent',
22
+ };