@sentropic/auth-ui 0.2.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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/contracts.d.ts +7 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +7 -0
- package/dist/contracts.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +5 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +180 -0
- package/dist/labels.js.map +1 -0
- package/dist/transport-fetch.d.ts +25 -0
- package/dist/transport-fetch.d.ts.map +1 -0
- package/dist/transport-fetch.js +98 -0
- package/dist/transport-fetch.js.map +1 -0
- package/dist/transport-types.d.ts +77 -0
- package/dist/transport-types.d.ts.map +1 -0
- package/dist/transport-types.js +2 -0
- package/dist/transport-types.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +29 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +137 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webauthn.d.ts +26 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +76 -0
- package/dist/webauthn.js.map +1 -0
- package/package.json +86 -0
- package/src/components/AuthDevicePair.svelte +173 -0
- package/src/components/AuthDevicePair.svelte.d.ts +17 -0
- package/src/components/AuthDevices.svelte +313 -0
- package/src/components/AuthDevices.svelte.d.ts +18 -0
- package/src/components/AuthLogin.svelte +222 -0
- package/src/components/AuthLogin.svelte.d.ts +18 -0
- package/src/components/AuthMagicLinkVerify.svelte +165 -0
- package/src/components/AuthMagicLinkVerify.svelte.d.ts +20 -0
- package/src/components/AuthRegister.svelte +394 -0
- package/src/components/AuthRegister.svelte.d.ts +25 -0
- package/src/contracts.ts +6 -0
- package/src/errors.ts +18 -0
- package/src/index.ts +2 -0
- package/src/labels.ts +186 -0
- package/src/transport-fetch.ts +170 -0
- package/src/transport-types.ts +105 -0
- package/src/transport.ts +33 -0
- package/src/types.ts +153 -0
- package/src/webauthn.ts +133 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
createDefaultAuthUiLabels,
|
|
5
|
+
type AuthUiError,
|
|
6
|
+
type AuthUiLabels,
|
|
7
|
+
type AuthUiSession,
|
|
8
|
+
type AuthUiTransport,
|
|
9
|
+
} from '../contracts.js';
|
|
10
|
+
import {
|
|
11
|
+
isWebAuthnSupported,
|
|
12
|
+
startPasskeyAuthentication,
|
|
13
|
+
getWebAuthnErrorMessage,
|
|
14
|
+
} from '../webauthn.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
transport: AuthUiTransport;
|
|
18
|
+
labels?: Partial<AuthUiLabels>;
|
|
19
|
+
onLoggedIn: (session: AuthUiSession) => void | Promise<void>;
|
|
20
|
+
onLostDevice?: () => void;
|
|
21
|
+
onError?: (error: AuthUiError) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { transport, labels, onLoggedIn, onLostDevice, onError }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const resolvedLabels = $derived(createDefaultAuthUiLabels(labels ?? {}));
|
|
27
|
+
|
|
28
|
+
let loading = $state(false);
|
|
29
|
+
let error = $state('');
|
|
30
|
+
let webauthnSupported = $state(false);
|
|
31
|
+
let showLostDevice = $state(false);
|
|
32
|
+
|
|
33
|
+
onMount(() => {
|
|
34
|
+
webauthnSupported = isWebAuthnSupported();
|
|
35
|
+
if (!webauthnSupported) {
|
|
36
|
+
error = resolvedLabels.loginUnsupportedBrowser;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function handleLogin(): Promise<void> {
|
|
41
|
+
loading = true;
|
|
42
|
+
error = '';
|
|
43
|
+
try {
|
|
44
|
+
const optionsResult = await transport.createPasskeyAuthenticationOptions({});
|
|
45
|
+
if (!optionsResult.ok) {
|
|
46
|
+
error = optionsResult.error.message;
|
|
47
|
+
onError?.(optionsResult.error);
|
|
48
|
+
loading = false;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let credential;
|
|
53
|
+
try {
|
|
54
|
+
credential = await startPasskeyAuthentication(optionsResult.value.options);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const authError = err as AuthUiError;
|
|
57
|
+
error = getWebAuthnErrorMessage(authError, resolvedLabels);
|
|
58
|
+
onError?.(authError);
|
|
59
|
+
loading = false;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const verifyResult = await transport.verifyPasskeyAuthentication({ credential });
|
|
64
|
+
if (!verifyResult.ok) {
|
|
65
|
+
error = verifyResult.error.message;
|
|
66
|
+
onError?.(verifyResult.error);
|
|
67
|
+
loading = false;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await onLoggedIn(verifyResult.value);
|
|
72
|
+
} finally {
|
|
73
|
+
loading = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleLostDevice(): void {
|
|
78
|
+
showLostDevice = true;
|
|
79
|
+
onLostDevice?.();
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class="auth-ui-login">
|
|
84
|
+
<header class="auth-ui-header">
|
|
85
|
+
<h2 class="auth-ui-title">{resolvedLabels.loginTitle}</h2>
|
|
86
|
+
<p class="auth-ui-subtitle">
|
|
87
|
+
{webauthnSupported ? resolvedLabels.loginSupportedHint : resolvedLabels.loginUnavailable}
|
|
88
|
+
</p>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
{#if !webauthnSupported}
|
|
92
|
+
<div class="auth-ui-alert auth-ui-alert--error" role="alert">{error}</div>
|
|
93
|
+
{:else if !showLostDevice}
|
|
94
|
+
<div class="auth-ui-section">
|
|
95
|
+
{#if error}
|
|
96
|
+
<div class="auth-ui-alert auth-ui-alert--error" role="alert">{error}</div>
|
|
97
|
+
{/if}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="auth-ui-button auth-ui-button--primary"
|
|
101
|
+
onclick={handleLogin}
|
|
102
|
+
disabled={loading}
|
|
103
|
+
>
|
|
104
|
+
{loading ? resolvedLabels.loginButtonLoading : resolvedLabels.loginButton}
|
|
105
|
+
</button>
|
|
106
|
+
<div class="auth-ui-actions">
|
|
107
|
+
<button type="button" class="auth-ui-link" onclick={handleLostDevice}>
|
|
108
|
+
{resolvedLabels.loginLostDevice}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="auth-ui-actions">
|
|
112
|
+
<slot name="no-account">
|
|
113
|
+
<span class="auth-ui-link">{resolvedLabels.loginNoAccount}</span>
|
|
114
|
+
</slot>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
{:else}
|
|
118
|
+
<div class="auth-ui-section">
|
|
119
|
+
<div class="auth-ui-alert auth-ui-alert--info" role="status">
|
|
120
|
+
<h3 class="auth-ui-alert__title">{resolvedLabels.loginLostDeviceTitle}</h3>
|
|
121
|
+
<p>{resolvedLabels.webauthnRegisterNotice}</p>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="auth-ui-actions">
|
|
124
|
+
<slot name="register-new-device">
|
|
125
|
+
<span class="auth-ui-link">{resolvedLabels.loginRegisterNewDevice}</span>
|
|
126
|
+
</slot>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="auth-ui-actions">
|
|
129
|
+
<button type="button" class="auth-ui-link auth-ui-link--secondary" onclick={() => (showLostDevice = false)}>
|
|
130
|
+
{resolvedLabels.loginBackToLogin}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
{/if}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.auth-ui-login {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
gap: 1.5rem;
|
|
142
|
+
max-width: 28rem;
|
|
143
|
+
margin: 0 auto;
|
|
144
|
+
padding: 2rem 1rem;
|
|
145
|
+
font-family: var(--auth-font-family, system-ui, -apple-system, sans-serif);
|
|
146
|
+
color: var(--auth-text, #111827);
|
|
147
|
+
}
|
|
148
|
+
.auth-ui-header {
|
|
149
|
+
text-align: center;
|
|
150
|
+
}
|
|
151
|
+
.auth-ui-title {
|
|
152
|
+
margin: 0 0 0.5rem;
|
|
153
|
+
font-size: 1.5rem;
|
|
154
|
+
font-weight: 700;
|
|
155
|
+
}
|
|
156
|
+
.auth-ui-subtitle {
|
|
157
|
+
margin: 0;
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
color: var(--auth-muted, #6b7280);
|
|
160
|
+
}
|
|
161
|
+
.auth-ui-section {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 1rem;
|
|
165
|
+
}
|
|
166
|
+
.auth-ui-actions {
|
|
167
|
+
text-align: center;
|
|
168
|
+
}
|
|
169
|
+
.auth-ui-alert {
|
|
170
|
+
padding: 0.75rem 1rem;
|
|
171
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
172
|
+
font-size: 0.875rem;
|
|
173
|
+
}
|
|
174
|
+
.auth-ui-alert--error {
|
|
175
|
+
background: var(--auth-error-bg, #fef2f2);
|
|
176
|
+
color: var(--auth-error-text, #991b1b);
|
|
177
|
+
}
|
|
178
|
+
.auth-ui-alert--info {
|
|
179
|
+
background: var(--auth-info-bg, #eff6ff);
|
|
180
|
+
color: var(--auth-info-text, #1e3a8a);
|
|
181
|
+
}
|
|
182
|
+
.auth-ui-alert__title {
|
|
183
|
+
margin: 0 0 0.25rem;
|
|
184
|
+
font-size: 0.95rem;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
187
|
+
.auth-ui-button {
|
|
188
|
+
width: 100%;
|
|
189
|
+
padding: 0.625rem 1rem;
|
|
190
|
+
border: none;
|
|
191
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
192
|
+
font-size: 0.875rem;
|
|
193
|
+
font-weight: 500;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
.auth-ui-button:disabled {
|
|
197
|
+
opacity: 0.5;
|
|
198
|
+
cursor: not-allowed;
|
|
199
|
+
}
|
|
200
|
+
.auth-ui-button--primary {
|
|
201
|
+
background: var(--auth-primary, #4f46e5);
|
|
202
|
+
color: var(--auth-primary-text, #ffffff);
|
|
203
|
+
}
|
|
204
|
+
.auth-ui-button--primary:hover:not(:disabled) {
|
|
205
|
+
background: var(--auth-primary-hover, #4338ca);
|
|
206
|
+
}
|
|
207
|
+
.auth-ui-link {
|
|
208
|
+
background: none;
|
|
209
|
+
border: none;
|
|
210
|
+
color: var(--auth-link, #4f46e5);
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
font-size: 0.875rem;
|
|
213
|
+
font-weight: 500;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
}
|
|
216
|
+
.auth-ui-link:hover {
|
|
217
|
+
text-decoration: underline;
|
|
218
|
+
}
|
|
219
|
+
.auth-ui-link--secondary {
|
|
220
|
+
color: var(--auth-link-secondary, #6b7280);
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type {
|
|
3
|
+
AuthUiError,
|
|
4
|
+
AuthUiLabels,
|
|
5
|
+
AuthUiSession,
|
|
6
|
+
AuthUiTransport,
|
|
7
|
+
} from '../contracts.js';
|
|
8
|
+
|
|
9
|
+
export interface AuthLoginProps {
|
|
10
|
+
transport: AuthUiTransport;
|
|
11
|
+
labels?: Partial<AuthUiLabels>;
|
|
12
|
+
onLoggedIn: (session: AuthUiSession) => void | Promise<void>;
|
|
13
|
+
onLostDevice?: () => void;
|
|
14
|
+
onError?: (error: AuthUiError) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare const AuthLogin: Component<AuthLoginProps>;
|
|
18
|
+
export default AuthLogin;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
createAuthUiError,
|
|
5
|
+
createDefaultAuthUiLabels,
|
|
6
|
+
type AuthUiError,
|
|
7
|
+
type AuthUiLabels,
|
|
8
|
+
type AuthUiSession,
|
|
9
|
+
type AuthUiTransport,
|
|
10
|
+
} from '../contracts.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
transport: AuthUiTransport;
|
|
14
|
+
labels?: Partial<AuthUiLabels>;
|
|
15
|
+
/** Function the host supplies to read the token from its router/URL. */
|
|
16
|
+
tokenSource: () => string | null | undefined;
|
|
17
|
+
onVerified: (session: AuthUiSession) => void | Promise<void>;
|
|
18
|
+
/** Optional callback the host can use to drive its own redirect (e.g. to /dashboard). */
|
|
19
|
+
onRedirect?: () => void;
|
|
20
|
+
onError?: (error: AuthUiError) => void;
|
|
21
|
+
/** Delay before invoking onRedirect after a successful verify. Defaults to 1000 ms. */
|
|
22
|
+
redirectDelayMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
transport,
|
|
27
|
+
labels,
|
|
28
|
+
tokenSource,
|
|
29
|
+
onVerified,
|
|
30
|
+
onRedirect,
|
|
31
|
+
onError,
|
|
32
|
+
redirectDelayMs = 1000,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const resolvedLabels = $derived(createDefaultAuthUiLabels(labels ?? {}));
|
|
36
|
+
|
|
37
|
+
let loading = $state(true);
|
|
38
|
+
let success = $state(false);
|
|
39
|
+
let error = $state('');
|
|
40
|
+
|
|
41
|
+
onMount(async () => {
|
|
42
|
+
const token = tokenSource()?.trim();
|
|
43
|
+
if (!token) {
|
|
44
|
+
const missing = createAuthUiError('invalid_input', resolvedLabels.magicLinkErrorMissingToken);
|
|
45
|
+
error = missing.message;
|
|
46
|
+
onError?.(missing);
|
|
47
|
+
loading = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await transport.verifyMagicLink({ token });
|
|
52
|
+
loading = false;
|
|
53
|
+
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
error = result.error.message || resolvedLabels.magicLinkErrorVerifyFailed;
|
|
56
|
+
onError?.(result.error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
success = true;
|
|
61
|
+
await onVerified(result.value);
|
|
62
|
+
if (onRedirect) {
|
|
63
|
+
setTimeout(onRedirect, redirectDelayMs);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<div class="auth-ui-magic-link">
|
|
69
|
+
<header class="auth-ui-header">
|
|
70
|
+
<h2 class="auth-ui-title">{resolvedLabels.magicLinkTitle}</h2>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
{#if loading && !error}
|
|
74
|
+
<div class="auth-ui-loading" role="status">
|
|
75
|
+
<div class="auth-ui-spinner" aria-hidden="true"></div>
|
|
76
|
+
<p class="auth-ui-loading__label">{resolvedLabels.magicLinkVerifying}</p>
|
|
77
|
+
</div>
|
|
78
|
+
{:else if success}
|
|
79
|
+
<div class="auth-ui-alert auth-ui-alert--success" role="status">
|
|
80
|
+
<h3 class="auth-ui-alert__title">{resolvedLabels.magicLinkSuccessTitle}</h3>
|
|
81
|
+
<p>{resolvedLabels.redirectingDashboard}</p>
|
|
82
|
+
</div>
|
|
83
|
+
{:else if error}
|
|
84
|
+
<div class="auth-ui-alert auth-ui-alert--error" role="alert">
|
|
85
|
+
<h3 class="auth-ui-alert__title">{resolvedLabels.magicLinkErrorTitle}</h3>
|
|
86
|
+
<p>{error}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="auth-ui-actions">
|
|
89
|
+
<slot name="back-to-login">
|
|
90
|
+
<span class="auth-ui-link">{resolvedLabels.magicLinkBackToLogin}</span>
|
|
91
|
+
</slot>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
.auth-ui-magic-link {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 1.5rem;
|
|
101
|
+
max-width: 28rem;
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
padding: 2rem 1rem;
|
|
104
|
+
font-family: var(--auth-font-family, system-ui, -apple-system, sans-serif);
|
|
105
|
+
color: var(--auth-text, #111827);
|
|
106
|
+
}
|
|
107
|
+
.auth-ui-header {
|
|
108
|
+
text-align: center;
|
|
109
|
+
}
|
|
110
|
+
.auth-ui-title {
|
|
111
|
+
margin: 0;
|
|
112
|
+
font-size: 1.5rem;
|
|
113
|
+
font-weight: 700;
|
|
114
|
+
}
|
|
115
|
+
.auth-ui-loading {
|
|
116
|
+
text-align: center;
|
|
117
|
+
}
|
|
118
|
+
.auth-ui-spinner {
|
|
119
|
+
display: inline-block;
|
|
120
|
+
width: 3rem;
|
|
121
|
+
height: 3rem;
|
|
122
|
+
border: 2px solid transparent;
|
|
123
|
+
border-bottom-color: var(--auth-primary, #4f46e5);
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
animation: auth-ui-spin 0.75s linear infinite;
|
|
126
|
+
}
|
|
127
|
+
.auth-ui-loading__label {
|
|
128
|
+
margin-top: 1rem;
|
|
129
|
+
font-size: 0.875rem;
|
|
130
|
+
color: var(--auth-muted, #6b7280);
|
|
131
|
+
}
|
|
132
|
+
@keyframes auth-ui-spin {
|
|
133
|
+
to { transform: rotate(360deg); }
|
|
134
|
+
}
|
|
135
|
+
.auth-ui-alert {
|
|
136
|
+
padding: 1rem;
|
|
137
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
}
|
|
140
|
+
.auth-ui-alert--error {
|
|
141
|
+
background: var(--auth-error-bg, #fef2f2);
|
|
142
|
+
color: var(--auth-error-text, #991b1b);
|
|
143
|
+
}
|
|
144
|
+
.auth-ui-alert--success {
|
|
145
|
+
background: var(--auth-success-bg, #f0fdf4);
|
|
146
|
+
color: var(--auth-success-text, #166534);
|
|
147
|
+
}
|
|
148
|
+
.auth-ui-alert__title {
|
|
149
|
+
margin: 0 0 0.5rem;
|
|
150
|
+
font-size: 0.95rem;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
153
|
+
.auth-ui-actions {
|
|
154
|
+
text-align: center;
|
|
155
|
+
}
|
|
156
|
+
.auth-ui-link {
|
|
157
|
+
color: var(--auth-link, #4f46e5);
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
font-weight: 500;
|
|
160
|
+
text-decoration: none;
|
|
161
|
+
}
|
|
162
|
+
.auth-ui-link:hover {
|
|
163
|
+
text-decoration: underline;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type {
|
|
3
|
+
AuthUiError,
|
|
4
|
+
AuthUiLabels,
|
|
5
|
+
AuthUiSession,
|
|
6
|
+
AuthUiTransport,
|
|
7
|
+
} from '../contracts.js';
|
|
8
|
+
|
|
9
|
+
export interface AuthMagicLinkVerifyProps {
|
|
10
|
+
transport: AuthUiTransport;
|
|
11
|
+
labels?: Partial<AuthUiLabels>;
|
|
12
|
+
tokenSource: () => string | null | undefined;
|
|
13
|
+
onVerified: (session: AuthUiSession) => void | Promise<void>;
|
|
14
|
+
onRedirect?: () => void;
|
|
15
|
+
onError?: (error: AuthUiError) => void;
|
|
16
|
+
redirectDelayMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const AuthMagicLinkVerify: Component<AuthMagicLinkVerifyProps>;
|
|
20
|
+
export default AuthMagicLinkVerify;
|