@nebulr-group/bridge-svelte 0.1.0-beta.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/auth/route-guard.d.ts +2 -0
  4. package/dist/auth/route-guard.js +28 -0
  5. package/dist/client/BridgeBootstrap.d.ts +8 -0
  6. package/dist/client/BridgeBootstrap.js +105 -0
  7. package/dist/client/BridgeBootstrap.svelte +43 -0
  8. package/dist/client/BridgeBootstrap.svelte.d.ts +6 -0
  9. package/dist/client/components/FeatureFlag.svelte +37 -0
  10. package/dist/client/components/FeatureFlag.svelte.d.ts +15 -0
  11. package/dist/client/components/ProfileName.svelte +18 -0
  12. package/dist/client/components/ProfileName.svelte.d.ts +6 -0
  13. package/dist/client/components/developer/ApiTokenManagement.svelte +365 -0
  14. package/dist/client/components/developer/ApiTokenManagement.svelte.d.ts +6 -0
  15. package/dist/client/components/sdk-auth/ForgotPassword.svelte +170 -0
  16. package/dist/client/components/sdk-auth/ForgotPassword.svelte.d.ts +10 -0
  17. package/dist/client/components/sdk-auth/LoginForm.svelte +414 -0
  18. package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +25 -0
  19. package/dist/client/components/sdk-auth/MagicLink.svelte +92 -0
  20. package/dist/client/components/sdk-auth/MagicLink.svelte.d.ts +9 -0
  21. package/dist/client/components/sdk-auth/MfaChallenge.svelte +150 -0
  22. package/dist/client/components/sdk-auth/MfaChallenge.svelte.d.ts +9 -0
  23. package/dist/client/components/sdk-auth/MfaSetup.svelte +182 -0
  24. package/dist/client/components/sdk-auth/MfaSetup.svelte.d.ts +8 -0
  25. package/dist/client/components/sdk-auth/PasskeyLogin.svelte +99 -0
  26. package/dist/client/components/sdk-auth/PasskeyLogin.svelte.d.ts +10 -0
  27. package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte +83 -0
  28. package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte.d.ts +7 -0
  29. package/dist/client/components/sdk-auth/PasskeySetup.svelte +151 -0
  30. package/dist/client/components/sdk-auth/PasskeySetup.svelte.d.ts +11 -0
  31. package/dist/client/components/sdk-auth/SignupForm.svelte +119 -0
  32. package/dist/client/components/sdk-auth/SignupForm.svelte.d.ts +14 -0
  33. package/dist/client/components/sdk-auth/SsoButton.svelte +70 -0
  34. package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +13 -0
  35. package/dist/client/components/sdk-auth/SsoProviderIcon.svelte +56 -0
  36. package/dist/client/components/sdk-auth/SsoProviderIcon.svelte.d.ts +7 -0
  37. package/dist/client/components/sdk-auth/TenantSelector.svelte +83 -0
  38. package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +11 -0
  39. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +108 -0
  40. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +16 -0
  41. package/dist/client/components/sdk-auth/shared/Alert.svelte +17 -0
  42. package/dist/client/components/sdk-auth/shared/Alert.svelte.d.ts +9 -0
  43. package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte +23 -0
  44. package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte.d.ts +10 -0
  45. package/dist/client/components/sdk-auth/shared/Spinner.svelte +16 -0
  46. package/dist/client/components/sdk-auth/shared/Spinner.svelte.d.ts +7 -0
  47. package/dist/client/components/subscription/PlanSelector.svelte +204 -0
  48. package/dist/client/components/subscription/PlanSelector.svelte.d.ts +22 -0
  49. package/dist/client/components/team/TeamAddUserDialog.svelte +125 -0
  50. package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +17 -0
  51. package/dist/client/components/team/TeamConfirmDialog.svelte +80 -0
  52. package/dist/client/components/team/TeamConfirmDialog.svelte.d.ts +21 -0
  53. package/dist/client/components/team/TeamEditUserDialog.svelte +131 -0
  54. package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +19 -0
  55. package/dist/client/components/team/TeamManagementPanel.svelte +72 -0
  56. package/dist/client/components/team/TeamManagementPanel.svelte.d.ts +20 -0
  57. package/dist/client/components/team/TeamProfileForm.svelte +129 -0
  58. package/dist/client/components/team/TeamProfileForm.svelte.d.ts +7 -0
  59. package/dist/client/components/team/TeamUserActionsMenu.svelte +63 -0
  60. package/dist/client/components/team/TeamUserActionsMenu.svelte.d.ts +9 -0
  61. package/dist/client/components/team/TeamUserList.svelte +226 -0
  62. package/dist/client/components/team/TeamUserList.svelte.d.ts +7 -0
  63. package/dist/client/components/team/TeamWorkspaceForm.svelte +134 -0
  64. package/dist/client/components/team/TeamWorkspaceForm.svelte.d.ts +7 -0
  65. package/dist/client/stores/config.store.d.ts +6 -0
  66. package/dist/client/stores/config.store.js +76 -0
  67. package/dist/client/utils/cn.d.ts +1 -0
  68. package/dist/client/utils/cn.js +3 -0
  69. package/dist/core/bridge-instance.d.ts +52 -0
  70. package/dist/core/bridge-instance.js +160 -0
  71. package/dist/index.d.ts +34 -0
  72. package/dist/index.js +39 -0
  73. package/dist/shared/feature-flag.d.ts +6 -0
  74. package/dist/shared/feature-flag.js +13 -0
  75. package/dist/shared/logger.d.ts +18 -0
  76. package/dist/shared/logger.js +32 -0
  77. package/dist/shared/profile.d.ts +18 -0
  78. package/dist/shared/profile.js +25 -0
  79. package/dist/shared/types/config.d.ts +42 -0
  80. package/dist/shared/types/config.js +1 -0
  81. package/dist/styles.css +1089 -0
  82. package/package.json +86 -0
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ onVerified?: () => void;
4
+ onError?: (error: Error) => void;
5
+ showRecoveryOption?: boolean;
6
+ }
7
+ declare const MfaChallenge: import("svelte").Component<Props, {}, "">;
8
+ type MfaChallenge = ReturnType<typeof MfaChallenge>;
9
+ export default MfaChallenge;
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
4
+ import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
5
+ import Spinner from './shared/Spinner.svelte';
6
+ import Alert from './shared/Alert.svelte';
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ onComplete?: () => void;
10
+ onError?: (error: Error) => void;
11
+ }
12
+
13
+ let {
14
+ onComplete,
15
+ onError,
16
+ class: className,
17
+ style,
18
+ ...rest
19
+ }: Props = $props();
20
+
21
+ let step = $state<'phone' | 'verify' | 'backup'>('phone');
22
+ let phoneNumber = $state('');
23
+ let code = $state('');
24
+ let backupCode = $state<string | null>(null);
25
+ let loading = $state(false);
26
+ let error = $state<string | null>(null);
27
+ let copied = $state(false);
28
+ let resendCountdown = $state(0);
29
+ let resendTimer: ReturnType<typeof setInterval> | null = null;
30
+
31
+ function startResendCooldown() {
32
+ resendCountdown = 60;
33
+ if (resendTimer) clearInterval(resendTimer);
34
+ resendTimer = setInterval(() => {
35
+ resendCountdown--;
36
+ if (resendCountdown <= 0) {
37
+ if (resendTimer) clearInterval(resendTimer);
38
+ resendTimer = null;
39
+ }
40
+ }, 1000);
41
+ }
42
+
43
+ async function handleSendCode() {
44
+ if (loading) return;
45
+ error = null;
46
+ loading = true;
47
+ try {
48
+ await getBridgeAuth().setupMfa(phoneNumber);
49
+ step = 'verify';
50
+ startResendCooldown();
51
+ } catch (err: any) {
52
+ error = err.message || 'Failed to send verification code.';
53
+ onError?.(err);
54
+ } finally {
55
+ loading = false;
56
+ }
57
+ }
58
+
59
+ async function handleResendCode() {
60
+ if (loading || resendCountdown > 0) return;
61
+ error = null;
62
+ loading = true;
63
+ try {
64
+ await getBridgeAuth().setupMfa(phoneNumber);
65
+ code = '';
66
+ startResendCooldown();
67
+ } catch (err: any) {
68
+ error = err.message || 'Failed to resend verification code.';
69
+ onError?.(err);
70
+ } finally {
71
+ loading = false;
72
+ }
73
+ }
74
+
75
+ function goBackToPhone() {
76
+ step = 'phone';
77
+ code = '';
78
+ error = null;
79
+ resendCountdown = 0;
80
+ if (resendTimer) clearInterval(resendTimer);
81
+ resendTimer = null;
82
+ }
83
+
84
+ async function handleVerifyCode() {
85
+ if (loading) return;
86
+ error = null;
87
+ loading = true;
88
+ try {
89
+ const result = await getBridgeAuth().confirmMfaSetup(code);
90
+ backupCode = result.backupCode ?? null;
91
+ step = 'backup';
92
+ } catch (err: any) {
93
+ error = err.message || 'Invalid code. Please try again.';
94
+ onError?.(err);
95
+ } finally {
96
+ loading = false;
97
+ }
98
+ }
99
+
100
+ async function copyBackupCode() {
101
+ if (backupCode) {
102
+ await navigator.clipboard.writeText(backupCode);
103
+ copied = true;
104
+ setTimeout(() => { copied = false; }, 2000);
105
+ }
106
+ }
107
+
108
+ async function handleDone() {
109
+ // Complete the MFA setup flow (transitions auth state + auto-selects tenant)
110
+ await getBridgeAuth().completeMfaSetup();
111
+ onComplete?.();
112
+ }
113
+ </script>
114
+
115
+ <AuthFormWrapper heading="Set up two-factor authentication" class={className} {style} {...rest}>
116
+ {#if error}
117
+ <Alert variant="error">{error}</Alert>
118
+ {/if}
119
+
120
+ {#if step === 'phone'}
121
+ <p class="bridge-step-desc">Enter your phone number to receive a verification code via SMS.</p>
122
+ <form onsubmit={(e) => { e.preventDefault(); handleSendCode(); }}>
123
+ <div class="bridge-form-group">
124
+ <label for="mfa-phone">Phone number</label>
125
+ <input
126
+ id="mfa-phone"
127
+ type="tel"
128
+ placeholder="+1 (555) 000-0000"
129
+ bind:value={phoneNumber}
130
+ disabled={loading}
131
+ />
132
+ </div>
133
+ <button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !phoneNumber.trim()}>
134
+ {#if loading}<Spinner size={16} />{:else}Send code{/if}
135
+ </button>
136
+ </form>
137
+
138
+ {:else if step === 'verify'}
139
+ <p class="bridge-step-desc">Enter the 6-digit code sent to your phone.</p>
140
+ <form onsubmit={(e) => { e.preventDefault(); handleVerifyCode(); }}>
141
+ <div class="bridge-form-group">
142
+ <label for="mfa-verify-code">Verification code</label>
143
+ <input
144
+ id="mfa-verify-code"
145
+ type="text"
146
+ inputmode="numeric"
147
+ autocomplete="one-time-code"
148
+ placeholder="Enter 6-digit code"
149
+ maxlength={6}
150
+ bind:value={code}
151
+ disabled={loading}
152
+ />
153
+ </div>
154
+ <button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || code.length < 6}>
155
+ {#if loading}<Spinner size={16} />{:else}Verify{/if}
156
+ </button>
157
+ </form>
158
+ <p class="bridge-mfa-help">
159
+ {#if resendCountdown > 0}
160
+ Didn't get your text message? You can resend in {resendCountdown}s.
161
+ {:else}
162
+ Didn't get your text message? <button type="button" class="bridge-link" onclick={handleResendCode} disabled={loading}>Resend code</button>.
163
+ {/if}
164
+ </p>
165
+ <button type="button" class="bridge-link" onclick={goBackToPhone}>Change phone number</button>
166
+
167
+ {:else if step === 'backup'}
168
+ <Alert variant="success">Two-factor authentication enabled!</Alert>
169
+ <p class="bridge-step-desc">Save this recovery code in a safe place. You can use it to access your account if you lose your phone.</p>
170
+ {#if backupCode}
171
+ <div class="bridge-backup-code">
172
+ <code>{backupCode}</code>
173
+ <button type="button" class="bridge-btn bridge-btn-secondary" onclick={copyBackupCode}>
174
+ {copied ? 'Copied!' : 'Copy'}
175
+ </button>
176
+ </div>
177
+ {/if}
178
+ <button type="button" class="bridge-btn bridge-btn-primary" onclick={handleDone}>
179
+ Done
180
+ </button>
181
+ {/if}
182
+ </AuthFormWrapper>
@@ -0,0 +1,8 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ onComplete?: () => void;
4
+ onError?: (error: Error) => void;
5
+ }
6
+ declare const MfaSetup: import("svelte").Component<Props, {}, "">;
7
+ type MfaSetup = ReturnType<typeof MfaSetup>;
8
+ export default MfaSetup;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import { onMount } from 'svelte';
4
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
5
+ import Spinner from './shared/Spinner.svelte';
6
+ import Alert from './shared/Alert.svelte';
7
+
8
+ interface Props extends HTMLButtonAttributes {
9
+ onLogin?: () => void;
10
+ onError?: (error: Error) => void;
11
+ onSetupPasskey?: () => void;
12
+ autofill?: boolean;
13
+ }
14
+
15
+ let {
16
+ onLogin,
17
+ onError,
18
+ onSetupPasskey,
19
+ autofill = false,
20
+ class: className,
21
+ style,
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ let loading = $state(false);
26
+ let error = $state<string | null>(null);
27
+ let supported = $state(false);
28
+
29
+ onMount(() => {
30
+ supported = typeof window !== 'undefined'
31
+ && !!window.PublicKeyCredential;
32
+ });
33
+
34
+ async function handlePasskeyLogin() {
35
+ if (loading || !supported) return;
36
+ error = null;
37
+ loading = true;
38
+ try {
39
+ const options = await getBridgeAuth().getPasskeyAuthOptions();
40
+
41
+ let authResponse: any;
42
+ if (typeof window !== 'undefined' && (window as any).__simpleWebAuthn?.startAuthentication) {
43
+ authResponse = await (window as any).__simpleWebAuthn.startAuthentication(options, autofill);
44
+ } else {
45
+ // @ts-ignore — @simplewebauthn/browser is an optional peer dependency
46
+ const mod = await import('@simplewebauthn/browser');
47
+ authResponse = await mod.startAuthentication({ optionsJSON: options, useBrowserAutofill: autofill });
48
+ }
49
+
50
+ await getBridgeAuth().authenticateWithPasskey(authResponse);
51
+ onLogin?.();
52
+ } catch (err: any) {
53
+ if (err.name === 'NotAllowedError' && onSetupPasskey) {
54
+ onSetupPasskey();
55
+ return;
56
+ }
57
+ if (err.name === 'NotAllowedError') {
58
+ error = 'Passkey authentication was cancelled.';
59
+ } else {
60
+ error = err.message || 'Passkey authentication failed.';
61
+ }
62
+ onError?.(err);
63
+ } finally {
64
+ loading = false;
65
+ }
66
+ }
67
+ </script>
68
+
69
+ {#if supported}
70
+ {#if error}
71
+ <Alert variant="error">{error}</Alert>
72
+ {#if onSetupPasskey}
73
+ <p class="bridge-passkey-setup-hint">
74
+ Don't have a passkey? <button type="button" class="bridge-link" onclick={onSetupPasskey}>Create one.</button>
75
+ </p>
76
+ {/if}
77
+ {/if}
78
+ <button
79
+ type="button"
80
+ class={className}
81
+ {style}
82
+ data-bridge-passkey-login
83
+ data-loading={loading}
84
+ onclick={handlePasskeyLogin}
85
+ disabled={loading}
86
+ {...rest}
87
+ >
88
+ <span class="bridge-sso-btn-inner">
89
+ {#if loading}
90
+ <Spinner size={16} />
91
+ {:else}
92
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
93
+ <path fill="currentColor" stroke-width="0.5" d="M3 20v-2.35c0-.63335.158335-1.175.475-1.625.316665-.45.725-.79165 1.225-1.025 1.11665-.5 2.1875-.875 3.2125-1.125S9.96665 13.5 11 13.5c.43335 0 .85415.02085 1.2625.0625s.82915.10415 1.2625.1875c-.08335.96665.09585 1.87915.5375 2.7375C14.50415 17.34585 15.15 18.01665 16 18.5v1.5H3Zm16 3.675-1.5-1.5v-4.65c-.73335-.21665-1.33335-.62915-1.8-1.2375-.46665-.60835-.7-1.3125-.7-2.1125 0-.96665.34165-1.79165 1.025-2.475.68335-.68335 1.50835-1.025 2.475-1.025s1.79165.34165 2.475 1.025c.68335.68335 1.025 1.50835 1.025 2.475 0 .75-.2125 1.41665-.6375 2-.425.58335-.9625 1-1.6125 1.25l1.25 1.25-1.5 1.5 1.5 1.5-2 2ZM11 11.5c-1.05 0-1.9375-.3625-2.6625-1.0875-.725-.725-1.0875-1.6125-1.0875-2.6625s.3625-1.9375 1.0875-2.6625C9.0625 4.3625 9.95 4 11 4s1.9375.3625 2.6625 1.0875c.725.725 1.0875 1.6125 1.0875 2.6625s-.3625 1.9375-1.0875 2.6625C12.9375 11.1375 12.05 11.5 11 11.5Zm7.5 3.175c.28335 0 .52085-.09585.7125-.2875S19.5 13.95835 19.5 13.675c0-.28335-.09585-.52085-.2875-.7125s-.42915-.2875-.7125-.2875c-.28335 0-.52085.09585-.7125.2875S17.5 13.39165 17.5 13.675c0 .28335.09585.52085.2875.7125s.42915.2875.7125.2875Z"/>
94
+ </svg>
95
+ {/if}
96
+ <span>Sign in with Passkeys</span>
97
+ </span>
98
+ </button>
99
+ {/if}
@@ -0,0 +1,10 @@
1
+ import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLButtonAttributes {
3
+ onLogin?: () => void;
4
+ onError?: (error: Error) => void;
5
+ onSetupPasskey?: () => void;
6
+ autofill?: boolean;
7
+ }
8
+ declare const PasskeyLogin: import("svelte").Component<Props, {}, "">;
9
+ type PasskeyLogin = ReturnType<typeof PasskeyLogin>;
10
+ export default PasskeyLogin;
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
4
+ import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
5
+ import Spinner from './shared/Spinner.svelte';
6
+ import Alert from './shared/Alert.svelte';
7
+
8
+ interface Props {
9
+ initialEmail?: string;
10
+ onBack: () => void;
11
+ }
12
+
13
+ let { initialEmail = '', onBack }: Props = $props();
14
+
15
+ type ViewState = 'form' | 'sent';
16
+
17
+ let view = $state<ViewState>('form');
18
+ let email = $state('');
19
+ let loading = $state(false);
20
+
21
+ // Seed email from prop once (runs at component creation)
22
+ untrack(() => { email = initialEmail; });
23
+ let error = $state<string | null>(null);
24
+
25
+ async function handleSubmit() {
26
+ if (loading || !email.trim()) return;
27
+ error = null;
28
+ loading = true;
29
+ try {
30
+ await getBridgeAuth().requestPasskeySetupLink(email);
31
+ view = 'sent';
32
+ } catch (err: any) {
33
+ error = err.message || 'Failed to send setup link.';
34
+ } finally {
35
+ loading = false;
36
+ }
37
+ }
38
+ </script>
39
+
40
+ {#if view === 'sent'}
41
+ <AuthFormWrapper heading="Check your email">
42
+ <p class="bridge-step-desc">
43
+ We sent a passkey setup link to <strong>{email}</strong>. Please check your inbox and click the link to create your passkey and finish signing in.
44
+ </p>
45
+ <button type="button" class="bridge-btn bridge-btn-secondary" onclick={onBack}>
46
+ Back to login
47
+ </button>
48
+ </AuthFormWrapper>
49
+ {:else}
50
+ <AuthFormWrapper heading="Create a passkey">
51
+ <p class="bridge-step-desc">
52
+ Enter your email and we will send a link to create your passkey.
53
+ </p>
54
+ {#if error}
55
+ <Alert variant="error">{error}</Alert>
56
+ {/if}
57
+ <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
58
+ <div class="bridge-form-group">
59
+ <label for="passkey-request-email">Email</label>
60
+ <input
61
+ id="passkey-request-email"
62
+ type="email"
63
+ placeholder="you@example.com"
64
+ required
65
+ bind:value={email}
66
+ disabled={loading}
67
+ />
68
+ </div>
69
+ <button
70
+ type="submit"
71
+ class="bridge-btn bridge-btn-primary"
72
+ disabled={loading || !email.trim()}
73
+ >
74
+ {#if loading}<Spinner size={16} />{:else}Send setup link{/if}
75
+ </button>
76
+ </form>
77
+ <div class="bridge-form-footer">
78
+ <button type="button" class="bridge-link" onclick={onBack}>
79
+ &larr; Back to login
80
+ </button>
81
+ </div>
82
+ </AuthFormWrapper>
83
+ {/if}
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ initialEmail?: string;
3
+ onBack: () => void;
4
+ }
5
+ declare const PasskeyRequestSetupLink: import("svelte").Component<Props, {}, "">;
6
+ type PasskeyRequestSetupLink = ReturnType<typeof PasskeyRequestSetupLink>;
7
+ export default PasskeyRequestSetupLink;
@@ -0,0 +1,151 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { onMount } from 'svelte';
4
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
5
+ import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
6
+ import Spinner from './shared/Spinner.svelte';
7
+ import Alert from './shared/Alert.svelte';
8
+
9
+ interface Props extends HTMLAttributes<HTMLDivElement> {
10
+ token: string;
11
+ onComplete?: () => void;
12
+ onError?: (error: Error) => void;
13
+ onBack?: () => void;
14
+ onExpired?: () => void;
15
+ }
16
+
17
+ let {
18
+ token,
19
+ onComplete,
20
+ onError,
21
+ onBack,
22
+ onExpired,
23
+ class: className,
24
+ style,
25
+ ...rest
26
+ }: Props = $props();
27
+
28
+ type ErrorType = 'expired' | 'cancelled' | 'unsupported' | 'network' | 'general';
29
+ type ViewState = 'loading' | 'success' | 'error';
30
+
31
+ let viewState = $state<ViewState>('loading');
32
+ let errorType = $state<ErrorType>('general');
33
+ let errorMessage = $state('');
34
+ let supported = $state(true);
35
+
36
+ function classifyError(err: any): ErrorType {
37
+ if (err.name === 'NotAllowedError') return 'cancelled';
38
+ const msg = (err.message || '').toLowerCase();
39
+ if (msg.includes('expired') || msg.includes('invalid token') || msg.includes('not found')) return 'expired';
40
+ if (msg.includes('network') || msg.includes('fetch') || msg.includes('econnrefused')) return 'network';
41
+ return 'general';
42
+ }
43
+
44
+ let heading = $derived(
45
+ viewState === 'loading' ? 'Setting up passkey'
46
+ : viewState === 'success' ? 'Passkey created'
47
+ : 'Passkey setup'
48
+ );
49
+
50
+ async function handleSetup() {
51
+ viewState = 'loading';
52
+ try {
53
+ const options = await getBridgeAuth().getPasskeyRegistrationOptions(token);
54
+
55
+ let regResponse: any;
56
+ if (typeof window !== 'undefined' && (window as any).__simpleWebAuthn?.startRegistration) {
57
+ regResponse = await (window as any).__simpleWebAuthn.startRegistration(options);
58
+ } else {
59
+ // @ts-ignore — @simplewebauthn/browser is an optional peer dependency
60
+ const mod = await import('@simplewebauthn/browser');
61
+ regResponse = await mod.startRegistration({ optionsJSON: options });
62
+ }
63
+
64
+ const result = await getBridgeAuth().verifyPasskeyRegistration(regResponse, token);
65
+ if (result.verified) {
66
+ viewState = 'success';
67
+ } else {
68
+ errorType = 'general';
69
+ errorMessage = 'Passkey registration could not be verified.';
70
+ viewState = 'error';
71
+ }
72
+ } catch (err: any) {
73
+ errorType = classifyError(err);
74
+ errorMessage = err.message || 'Passkey setup failed.';
75
+ viewState = 'error';
76
+ onError?.(err);
77
+ }
78
+ }
79
+
80
+ onMount(() => {
81
+ if (typeof window === 'undefined' || !window.PublicKeyCredential) {
82
+ supported = false;
83
+ errorType = 'unsupported';
84
+ viewState = 'error';
85
+ return;
86
+ }
87
+ handleSetup();
88
+ });
89
+ </script>
90
+
91
+ <AuthFormWrapper {heading} class={className} {style} {...rest}>
92
+ {#if viewState === 'loading'}
93
+ <div class="bridge-passkey-loading">
94
+ <Spinner size={16} />
95
+ <p class="bridge-step-desc">Follow the prompt from your browser or device to complete passkey setup.</p>
96
+ </div>
97
+
98
+ {:else if viewState === 'success'}
99
+ <Alert variant="success">Your passkey has been created.</Alert>
100
+ <button
101
+ type="button"
102
+ class="bridge-btn bridge-btn-primary"
103
+ onclick={() => onComplete?.()}
104
+ >
105
+ Sign in now
106
+ </button>
107
+
108
+ {:else if errorType === 'expired'}
109
+ <Alert variant="error">This setup link has expired or is invalid.</Alert>
110
+ {#if onExpired}
111
+ <button type="button" class="bridge-btn bridge-btn-primary" onclick={() => onExpired?.()}>
112
+ Request new setup link
113
+ </button>
114
+ {/if}
115
+ {#if onBack}
116
+ <button type="button" class="bridge-btn bridge-btn-ghost" onclick={() => onBack?.()}>
117
+ Back to sign in
118
+ </button>
119
+ {/if}
120
+
121
+ {:else if errorType === 'cancelled'}
122
+ <Alert variant="error">Passkey setup was cancelled.</Alert>
123
+ <button type="button" class="bridge-btn bridge-btn-primary" onclick={handleSetup}>
124
+ Try again
125
+ </button>
126
+ {#if onBack}
127
+ <button type="button" class="bridge-btn bridge-btn-ghost" onclick={() => onBack?.()}>
128
+ Back to sign in
129
+ </button>
130
+ {/if}
131
+
132
+ {:else if errorType === 'unsupported'}
133
+ <Alert variant="info">Your browser does not support passkeys.</Alert>
134
+ {#if onBack}
135
+ <button type="button" class="bridge-btn bridge-btn-secondary" onclick={() => onBack?.()}>
136
+ Back to sign in
137
+ </button>
138
+ {/if}
139
+
140
+ {:else}
141
+ <Alert variant="error">{errorMessage || 'An error occurred during passkey setup.'}</Alert>
142
+ <button type="button" class="bridge-btn bridge-btn-primary" onclick={handleSetup}>
143
+ Try again
144
+ </button>
145
+ {#if onBack}
146
+ <button type="button" class="bridge-btn bridge-btn-ghost" onclick={() => onBack?.()}>
147
+ Back to sign in
148
+ </button>
149
+ {/if}
150
+ {/if}
151
+ </AuthFormWrapper>
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ token: string;
4
+ onComplete?: () => void;
5
+ onError?: (error: Error) => void;
6
+ onBack?: () => void;
7
+ onExpired?: () => void;
8
+ }
9
+ declare const PasskeySetup: import("svelte").Component<Props, {}, "">;
10
+ type PasskeySetup = ReturnType<typeof PasskeySetup>;
11
+ export default PasskeySetup;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
5
+ import { getConfig } from '../../stores/config.store.js';
6
+ import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
7
+ import Spinner from './shared/Spinner.svelte';
8
+ import Alert from './shared/Alert.svelte';
9
+
10
+ interface Props extends HTMLAttributes<HTMLDivElement> {
11
+ onSignup?: () => void;
12
+ onError?: (error: Error) => void;
13
+ showLoginLink?: boolean;
14
+ /** Override login page URL. Default: derived from BridgeConfig loginRoute. */
15
+ loginHref?: string | undefined;
16
+ heading?: string;
17
+ footer?: Snippet;
18
+ }
19
+
20
+ let {
21
+ onSignup,
22
+ onError,
23
+ showLoginLink = true,
24
+ loginHref = undefined,
25
+ heading = 'Create your account',
26
+ footer,
27
+ class: className,
28
+ style,
29
+ ...rest
30
+ }: Props = $props();
31
+
32
+ let effectiveLoginHref = $derived(loginHref ?? getConfig().loginRoute);
33
+
34
+ let email = $state('');
35
+ let firstName = $state('');
36
+ let lastName = $state('');
37
+ let loading = $state(false);
38
+ let error = $state<string | null>(null);
39
+ let success = $state(false);
40
+
41
+ async function handleSubmit() {
42
+ if (loading) return;
43
+ error = null;
44
+ loading = true;
45
+ try {
46
+ await getBridgeAuth().signup(email, firstName, lastName);
47
+ success = true;
48
+ onSignup?.();
49
+ } catch (err: any) {
50
+ error = err.message || 'Failed to create account.';
51
+ onError?.(err);
52
+ } finally {
53
+ loading = false;
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <AuthFormWrapper {heading} class={className} {style} {...rest}>
59
+ {#if success}
60
+ <h2 class="bridge-success-heading">Check your email</h2>
61
+ <p class="bridge-step-desc">We sent a verification link to <strong>{email}</strong>. Check your inbox to activate your account.</p>
62
+ {#if footer}
63
+ {@render footer()}
64
+ {:else if showLoginLink}
65
+ <div class="bridge-form-footer">
66
+ Already have an account? <a href={effectiveLoginHref}>Log in</a>
67
+ </div>
68
+ {/if}
69
+ {:else}
70
+ {#if error}
71
+ <Alert variant="error">{error}</Alert>
72
+ {/if}
73
+
74
+ <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
75
+ <div class="bridge-form-group">
76
+ <label for="signup-email">Email</label>
77
+ <input
78
+ id="signup-email"
79
+ type="email"
80
+ placeholder="you@example.com"
81
+ required
82
+ bind:value={email}
83
+ disabled={loading}
84
+ />
85
+ </div>
86
+ <div class="bridge-form-group">
87
+ <label for="signup-first-name">First name</label>
88
+ <input
89
+ id="signup-first-name"
90
+ type="text"
91
+ placeholder="First name"
92
+ bind:value={firstName}
93
+ disabled={loading}
94
+ />
95
+ </div>
96
+ <div class="bridge-form-group">
97
+ <label for="signup-last-name">Last name</label>
98
+ <input
99
+ id="signup-last-name"
100
+ type="text"
101
+ placeholder="Last name"
102
+ bind:value={lastName}
103
+ disabled={loading}
104
+ />
105
+ </div>
106
+ <button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !email.trim()}>
107
+ {#if loading}<Spinner size={16} />{:else}Sign up{/if}
108
+ </button>
109
+ </form>
110
+
111
+ {#if footer}
112
+ {@render footer()}
113
+ {:else if showLoginLink}
114
+ <div class="bridge-form-footer">
115
+ Already have an account? <a href={effectiveLoginHref}>Log in</a>
116
+ </div>
117
+ {/if}
118
+ {/if}
119
+ </AuthFormWrapper>