@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +202 -0
- package/README.md +253 -2
- package/dist/auth.svelte.d.ts +170 -0
- package/dist/auth.svelte.js +208 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +6 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +35 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/kit/index.d.ts +55 -0
- package/dist/kit/index.js +121 -0
- package/dist/kit/watch.svelte.d.ts +10 -0
- package/dist/kit/watch.svelte.js +28 -0
- package/dist/navigation.d.ts +40 -0
- package/dist/navigation.js +22 -0
- package/dist/screens/AuthLayout.svelte +38 -0
- package/dist/screens/AuthLayout.svelte.d.ts +8 -0
- package/dist/screens/FallbackOptions.svelte +69 -0
- package/dist/screens/FallbackOptions.svelte.d.ts +13 -0
- package/dist/screens/Login.svelte +253 -0
- package/dist/screens/Login.svelte.d.ts +3 -0
- package/dist/screens/MagicLinkSent.svelte +74 -0
- package/dist/screens/MagicLinkSent.svelte.d.ts +3 -0
- package/dist/screens/OAuthCallback.svelte +69 -0
- package/dist/screens/OAuthCallback.svelte.d.ts +3 -0
- package/dist/screens/OAuthProviderButtons.svelte +49 -0
- package/dist/screens/OAuthProviderButtons.svelte.d.ts +6 -0
- package/dist/screens/OtpInput.svelte +101 -0
- package/dist/screens/OtpInput.svelte.d.ts +10 -0
- package/dist/screens/PasskeyLogin.svelte +34 -0
- package/dist/screens/PasskeyLogin.svelte.d.ts +3 -0
- package/dist/screens/RegisterPasskey.svelte +110 -0
- package/dist/screens/RegisterPasskey.svelte.d.ts +3 -0
- package/dist/screens/VerifyMagicLink.svelte +104 -0
- package/dist/screens/VerifyMagicLink.svelte.d.ts +3 -0
- package/dist/screens/VerifyOtp.svelte +141 -0
- package/dist/screens/VerifyOtp.svelte.d.ts +7 -0
- package/dist/seamless-auth.css +260 -0
- package/dist/styles.generated.d.ts +2 -0
- package/dist/styles.generated.js +3 -0
- 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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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,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;
|
package/dist/context.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|