index.ts1427 lines · main
1/**
2 * @briven/auth/vue — Vue 3 bindings for `@briven/auth`.
3 *
4 * import { createBrivenAuth } from '@briven/auth';
5 * import { BrivenAuthProvider, useSession, useUser } from '@briven/auth/vue';
6 *
7 * const auth = createBrivenAuth({ projectId: 'p_abc123', publicKey: '...' });
8 *
9 * <BrivenAuthProvider :value="auth">
10 * <App />
11 * </BrivenAuthProvider>
12 *
13 * function App() {
14 * const { session, isLoading } = useSession();
15 * return session ? <Home /> : <BrivenSignIn />;
16 * }
17 *
18 * Zero hard dependency on Nuxt — works in any Vue 3 environment.
19 */
20
21import {
22 type InjectionKey,
23 type PropType,
24 type Ref,
25 type VNode,
26 h,
27 inject,
28 onMounted,
29 provide,
30 ref,
31 watch,
32} from 'vue';
33
34import {
35 type BrivenAuthClient,
36 type ClientSession,
37 type MembershipRequest,
38 type OAuthProvider,
39 type Org,
40 type OrgDomain,
41 type OrgInvite,
42 type OrgMember,
43 type OrgPermission,
44 type OrgRole,
45 type Passkey,
46 providerLogoDataUri,
47 type SessionResponse,
48 type SignInResult,
49 type SimpleResult,
50 type SsoConnection,
51 type SsoProviderType,
52 type User,
53 type UserEmail,
54} from '../index.js';
55
56const BrivenAuthKey: InjectionKey<BrivenAuthClient> = Symbol('briven-auth');
57
58// ─── Provider ──────────────────────────────────────────────────────────────
59
60export interface BrivenAuthProviderProps {
61 value: BrivenAuthClient;
62}
63
64export const BrivenAuthProvider = {
65 name: 'BrivenAuthProvider',
66 props: {
67 value: { type: Object as PropType<BrivenAuthClient>, required: true },
68 },
69 setup(props: BrivenAuthProviderProps, { slots }: { slots: { default?: () => VNode[] } }) {
70 provide(BrivenAuthKey, props.value);
71 return () => (slots.default ? slots.default() : null);
72 },
73};
74
75/** Throws when called outside a `<BrivenAuthProvider>`. */
76export function useBrivenAuth(): BrivenAuthClient {
77 const client = inject(BrivenAuthKey);
78 if (!client) {
79 throw new Error('useBrivenAuth must be called inside <BrivenAuthProvider>');
80 }
81 return client;
82}
83
84// ─── Composables ───────────────────────────────────────────────────────────
85
86export interface UseSessionResult {
87 session: Ref<SessionResponse | null>;
88 isLoading: Ref<boolean>;
89 refresh: () => Promise<void>;
90}
91
92export function useSession(): UseSessionResult {
93 const client = useBrivenAuth();
94 const session = ref<SessionResponse | null>(null);
95 const isLoading = ref(true);
96
97 const refresh = async () => {
98 isLoading.value = true;
99 session.value = await client.getSession();
100 isLoading.value = false;
101 };
102
103 onMounted(() => {
104 void refresh();
105 });
106
107 return { session, isLoading, refresh };
108}
109
110export interface UseUserResult {
111 user: Ref<User | null>;
112 isLoading: Ref<boolean>;
113 refresh: () => Promise<void>;
114}
115
116export function useUser(): UseUserResult {
117 const client = useBrivenAuth();
118 const user = ref<User | null>(null);
119 const isLoading = ref(true);
120
121 const refresh = async () => {
122 isLoading.value = true;
123 user.value = await client.getUser();
124 isLoading.value = false;
125 };
126
127 onMounted(() => {
128 void refresh();
129 });
130
131 return { user, isLoading, refresh };
132}
133
134export interface UseUserMetadataResult {
135 metadata: Ref<Record<string, unknown> | null>;
136 isLoading: Ref<boolean>;
137 refresh: () => Promise<void>;
138 set: (patch: Record<string, unknown>) => Promise<void>;
139}
140
141export function useUserMetadata(): UseUserMetadataResult {
142 const client = useBrivenAuth();
143 const metadata = ref<Record<string, unknown> | null>(null);
144 const isLoading = ref(true);
145
146 const refresh = async () => {
147 isLoading.value = true;
148 const result = await client.user.getMetadata();
149 metadata.value = result.ok ? result.publicMetadata : null;
150 isLoading.value = false;
151 };
152
153 const set = async (patch: Record<string, unknown>) => {
154 const result = await client.user.setMetadata(patch);
155 if (result.ok) {
156 metadata.value = result.publicMetadata;
157 }
158 };
159
160 onMounted(() => {
161 void refresh();
162 });
163
164 return { metadata, isLoading, refresh, set };
165}
166
167export interface UseUserEmailsResult {
168 emails: Ref<UserEmail[] | null>;
169 isLoading: Ref<boolean>;
170 refresh: () => Promise<void>;
171 add: (email: string) => Promise<void>;
172 remove: (emailId: string) => Promise<void>;
173}
174
175export function useUserEmails(): UseUserEmailsResult {
176 const client = useBrivenAuth();
177 const emails = ref<UserEmail[] | null>(null);
178 const isLoading = ref(true);
179
180 const refresh = async () => {
181 isLoading.value = true;
182 const result = await client.user.listEmails();
183 emails.value = result.ok ? result.emails : null;
184 isLoading.value = false;
185 };
186
187 const add = async (email: string) => {
188 const result = await client.user.addEmail(email);
189 if (result.ok) await refresh();
190 };
191
192 const remove = async (emailId: string) => {
193 const result = await client.user.removeEmail(emailId);
194 if (result.ok) await refresh();
195 };
196
197 onMounted(() => {
198 void refresh();
199 });
200
201 return { emails, isLoading, refresh, add, remove };
202}
203
204export interface UseActiveOrganizationResult {
205 activeOrg: Ref<Org | null>;
206 isLoading: Ref<boolean>;
207 refresh: () => Promise<void>;
208 setActive: (orgId: string) => Promise<void>;
209}
210
211export function useActiveOrganization(): UseActiveOrganizationResult {
212 const client = useBrivenAuth();
213 const activeOrg = ref<Org | null>(null);
214 const isLoading = ref(true);
215
216 const refresh = async () => {
217 isLoading.value = true;
218 const result = await client.organization.getActive();
219 if (result.ok) activeOrg.value = result.data;
220 isLoading.value = false;
221 };
222
223 const setActive = async (orgId: string) => {
224 const result = await client.organization.setActive(orgId);
225 if (result.ok) await refresh();
226 };
227
228 onMounted(() => {
229 void refresh();
230 });
231
232 return { activeOrg, isLoading, refresh, setActive };
233}
234
235// ─── Shared helpers ────────────────────────────────────────────────────────
236
237function useRedirectToHosted(auth: BrivenAuthClient, redirectTo?: string, locale?: string) {
238 return (flow: 'sign-in' | 'sign-up' | 'magic-link') => {
239 const url = auth.hostedPageURL(flow, redirectTo, locale);
240 if (typeof window !== 'undefined') {
241 window.location.assign(url);
242 }
243 };
244}
245
246// ─── BrivenSignIn ──────────────────────────────────────────────────────────
247
248export interface BrivenSignInProps {
249 providers?: ReadonlyArray<OAuthProvider>;
250 showEmailPassword?: boolean;
251 showMagicLink?: boolean;
252 redirectTo?: string;
253 onSuccess?: (result: { userId: string }) => void;
254 className?: string;
255 mode?: 'direct' | 'hosted';
256 locale?: string;
257}
258
259const DEFAULT_PROVIDERS: ReadonlyArray<OAuthProvider> = [
260 'konnos',
261 'google',
262 'github',
263 'discord',
264 'microsoft',
265 'apple',
266 'twitter',
267 'linkedin',
268 'gitlab',
269];
270
271function oauthButtonChildren(provider: OAuthProvider): Array<VNode | string> | string {
272 const logo = providerLogoDataUri(provider);
273 const label = `continue with ${provider}`;
274 if (!logo) return label;
275 return [
276 h('img', {
277 src: logo,
278 alt: '',
279 width: 20,
280 height: 20,
281 'aria-hidden': true,
282 class: 'briven-auth-oauth-logo',
283 style: { width: '20px', height: '20px', objectFit: 'contain', flexShrink: '0' },
284 }),
285 label,
286 ];
287}
288
289export const BrivenSignIn = {
290 name: 'BrivenSignIn',
291 props: {
292 providers: { type: Array as PropType<ReadonlyArray<OAuthProvider>>, default: () => DEFAULT_PROVIDERS },
293 showEmailPassword: { type: Boolean, default: true },
294 showMagicLink: { type: Boolean, default: true },
295 redirectTo: { type: String, default: undefined },
296 onSuccess: { type: Function as PropType<(result: { userId: string }) => void>, default: undefined },
297 className: { type: String, default: undefined },
298 mode: { type: String as PropType<'direct' | 'hosted'>, default: 'direct' },
299 locale: { type: String, default: undefined },
300 },
301 setup(props: BrivenSignInProps) {
302 const auth = useBrivenAuth();
303 const email = ref('');
304 const password = ref('');
305 const magicEmail = ref('');
306 const pending = ref<'password' | 'magic' | null>(null);
307 const error = ref<string | null>(null);
308 const magicSent = ref(false);
309
310 const redirectToHosted = useRedirectToHosted(auth, props.redirectTo, props.locale);
311
312 const handlePassword = async (e: Event) => {
313 e.preventDefault();
314 if (props.mode === 'hosted') {
315 redirectToHosted('sign-in');
316 return;
317 }
318 pending.value = 'password';
319 error.value = null;
320 const result: SignInResult = await auth.signIn.email({ email: email.value, password: password.value });
321 if (result.ok && 'userId' in result) {
322 props.onSuccess?.({ userId: result.userId });
323 } else if (result.ok && 'twoFactorRequired' in result) {
324 error.value = 'two-factor required — complete the challenge';
325 } else if (!result.ok) {
326 error.value = result.message;
327 }
328 pending.value = null;
329 };
330
331 const handleMagic = async (e: Event) => {
332 e.preventDefault();
333 if (props.mode === 'hosted') {
334 redirectToHosted('magic-link');
335 return;
336 }
337 pending.value = 'magic';
338 error.value = null;
339 const result = await auth.signIn.magicLink({ email: magicEmail.value, redirectTo: props.redirectTo });
340 if (result.ok) {
341 magicSent.value = true;
342 } else {
343 error.value = result.message;
344 }
345 pending.value = null;
346 };
347
348 const handleOAuth = (provider: OAuthProvider) => {
349 const { redirectUrl } = auth.signIn.social({ provider, redirectTo: props.redirectTo });
350 if (typeof window !== 'undefined') {
351 window.location.assign(redirectUrl);
352 }
353 };
354
355 return () =>
356 h(
357 'div',
358 { class: props.className ?? 'briven-auth-signin', 'data-briven-auth': 'signin' },
359 [
360 props.showEmailPassword
361 ? h(
362 'form',
363 {
364 key: 'password',
365 onSubmit: handlePassword,
366 class: 'briven-auth-form',
367 'data-briven-auth-flow': 'password',
368 },
369 [
370 h('input', {
371 key: 'email',
372 type: 'email',
373 required: true,
374 placeholder: 'email',
375 value: email.value,
376 onInput: (e: Event) => { email.value = (e.target as HTMLInputElement).value; },
377 autocomplete: 'email',
378 class: 'briven-auth-input',
379 }),
380 h('input', {
381 key: 'password',
382 type: 'password',
383 required: true,
384 placeholder: 'password',
385 value: password.value,
386 onInput: (e: Event) => { password.value = (e.target as HTMLInputElement).value; },
387 autocomplete: 'current-password',
388 class: 'briven-auth-input',
389 }),
390 h(
391 'button',
392 {
393 key: 'submit',
394 type: 'submit',
395 disabled: pending.value !== null,
396 class: 'briven-auth-submit',
397 },
398 pending.value === 'password' ? 'signing in…' : 'sign in',
399 ),
400 ],
401 )
402 : null,
403 props.showMagicLink
404 ? magicSent.value
405 ? h('p', { key: 'magic-sent', class: 'briven-auth-message' }, 'check your inbox for the sign-in link.')
406 : h(
407 'form',
408 {
409 key: 'magic',
410 onSubmit: handleMagic,
411 class: 'briven-auth-form',
412 'data-briven-auth-flow': 'magic-link',
413 },
414 [
415 h('input', {
416 key: 'email',
417 type: 'email',
418 required: true,
419 placeholder: 'email for magic link',
420 value: magicEmail.value,
421 onInput: (e: Event) => { magicEmail.value = (e.target as HTMLInputElement).value; },
422 autocomplete: 'email',
423 class: 'briven-auth-input',
424 }),
425 h(
426 'button',
427 {
428 key: 'submit',
429 type: 'submit',
430 disabled: pending.value !== null,
431 class: 'briven-auth-submit',
432 },
433 pending.value === 'magic' ? 'sending…' : 'send magic link',
434 ),
435 ],
436 )
437 : null,
438 (props.providers ?? DEFAULT_PROVIDERS).length > 0
439 ? h(
440 'div',
441 { key: 'oauth', class: 'briven-auth-oauth', 'data-briven-auth-flow': 'oauth' },
442 (props.providers ?? DEFAULT_PROVIDERS).map((provider) =>
443 h(
444 'button',
445 {
446 key: provider,
447 type: 'button',
448 'data-briven-auth-provider': provider,
449 onClick: () => handleOAuth(provider),
450 class: 'briven-auth-oauth-button',
451 style: {
452 display: 'inline-flex',
453 alignItems: 'center',
454 justifyContent: 'center',
455 gap: '8px',
456 },
457 },
458 oauthButtonChildren(provider),
459 ),
460 ),
461 )
462 : null,
463 error.value
464 ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value)
465 : null,
466 ],
467 );
468 },
469};
470
471// ─── BrivenSignUp ──────────────────────────────────────────────────────────
472
473export interface BrivenSignUpProps {
474 providers?: ReadonlyArray<OAuthProvider>;
475 showEmailPassword?: boolean;
476 redirectTo?: string;
477 onSuccess?: (result: { userId: string }) => void;
478 className?: string;
479 mode?: 'direct' | 'hosted';
480 locale?: string;
481}
482
483export const BrivenSignUp = {
484 name: 'BrivenSignUp',
485 props: {
486 providers: { type: Array as PropType<ReadonlyArray<OAuthProvider>>, default: () => DEFAULT_PROVIDERS },
487 showEmailPassword: { type: Boolean, default: true },
488 redirectTo: { type: String, default: undefined },
489 onSuccess: { type: Function as PropType<(result: { userId: string }) => void>, default: undefined },
490 className: { type: String, default: undefined },
491 mode: { type: String as PropType<'direct' | 'hosted'>, default: 'direct' },
492 locale: { type: String, default: undefined },
493 },
494 setup(props: BrivenSignUpProps) {
495 const auth = useBrivenAuth();
496 const name = ref('');
497 const email = ref('');
498 const password = ref('');
499 const pending = ref(false);
500 const error = ref<string | null>(null);
501
502 const redirectToHosted = useRedirectToHosted(auth, props.redirectTo, props.locale);
503
504 const handleSubmit = async (e: Event) => {
505 e.preventDefault();
506 if (props.mode === 'hosted') {
507 redirectToHosted('sign-up');
508 return;
509 }
510 pending.value = true;
511 error.value = null;
512 const result: SignInResult = await auth.signUp.email({
513 email: email.value,
514 password: password.value,
515 name: name.value || undefined,
516 });
517 if (result.ok && 'userId' in result) {
518 props.onSuccess?.({ userId: result.userId });
519 } else if (result.ok && 'twoFactorRequired' in result) {
520 error.value = 'two-factor required — complete the challenge';
521 } else if (!result.ok) {
522 error.value = result.message;
523 }
524 pending.value = false;
525 };
526
527 const handleOAuth = (provider: OAuthProvider) => {
528 const { redirectUrl } = auth.signIn.social({ provider, redirectTo: props.redirectTo });
529 if (typeof window !== 'undefined') {
530 window.location.assign(redirectUrl);
531 }
532 };
533
534 return () =>
535 h(
536 'div',
537 { class: props.className ?? 'briven-auth-signup', 'data-briven-auth': 'signup' },
538 [
539 props.showEmailPassword
540 ? h(
541 'form',
542 {
543 key: 'password',
544 onSubmit: handleSubmit,
545 class: 'briven-auth-form',
546 'data-briven-auth-flow': 'password',
547 },
548 [
549 h('input', {
550 key: 'name',
551 type: 'text',
552 placeholder: 'name (optional)',
553 value: name.value,
554 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
555 autocomplete: 'name',
556 class: 'briven-auth-input',
557 }),
558 h('input', {
559 key: 'email',
560 type: 'email',
561 required: true,
562 placeholder: 'email',
563 value: email.value,
564 onInput: (e: Event) => { email.value = (e.target as HTMLInputElement).value; },
565 autocomplete: 'email',
566 class: 'briven-auth-input',
567 }),
568 h('input', {
569 key: 'password',
570 type: 'password',
571 required: true,
572 placeholder: 'password',
573 value: password.value,
574 onInput: (e: Event) => { password.value = (e.target as HTMLInputElement).value; },
575 autocomplete: 'new-password',
576 class: 'briven-auth-input',
577 }),
578 h(
579 'button',
580 {
581 key: 'submit',
582 type: 'submit',
583 disabled: pending.value,
584 class: 'briven-auth-submit',
585 },
586 pending.value ? 'creating account…' : 'create account',
587 ),
588 ],
589 )
590 : null,
591 (props.providers ?? DEFAULT_PROVIDERS).length > 0
592 ? h(
593 'div',
594 { key: 'oauth', class: 'briven-auth-oauth', 'data-briven-auth-flow': 'oauth' },
595 (props.providers ?? DEFAULT_PROVIDERS).map((provider) =>
596 h(
597 'button',
598 {
599 key: provider,
600 type: 'button',
601 'data-briven-auth-provider': provider,
602 onClick: () => handleOAuth(provider),
603 class: 'briven-auth-oauth-button',
604 style: {
605 display: 'inline-flex',
606 alignItems: 'center',
607 justifyContent: 'center',
608 gap: '8px',
609 },
610 },
611 oauthButtonChildren(provider),
612 ),
613 ),
614 )
615 : null,
616 error.value
617 ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value)
618 : null,
619 ],
620 );
621 },
622};
623
624// ─── UserButton ────────────────────────────────────────────────────────────
625
626export interface UserButtonProps {
627 className?: string;
628 profileUrl?: string;
629}
630
631export const UserButton = {
632 name: 'UserButton',
633 props: {
634 className: { type: String, default: undefined },
635 profileUrl: { type: String, default: undefined },
636 },
637 setup(props: UserButtonProps) {
638 const auth = useBrivenAuth();
639 const { user, isLoading } = useUser();
640 const open = ref(false);
641
642 const handleSignOut = async () => {
643 await auth.signOut();
644 if (typeof window !== 'undefined') {
645 window.location.reload();
646 }
647 };
648
649 const handleProfile = () => {
650 const url = props.profileUrl ?? auth.hostedPageURL('profile');
651 if (typeof window !== 'undefined') {
652 window.location.assign(url);
653 }
654 };
655
656 return () => {
657 if (isLoading.value || !user.value) return null;
658 const label = user.value.name ?? user.value.email;
659 return h(
660 'div',
661 { class: props.className ?? 'briven-auth-userbutton', 'data-briven-auth': 'userbutton' },
662 [
663 h(
664 'button',
665 {
666 type: 'button',
667 onClick: () => { open.value = !open.value; },
668 class: 'briven-auth-userbutton-trigger',
669 },
670 label,
671 ),
672 open.value
673 ? h(
674 'div',
675 { class: 'briven-auth-userbutton-dropdown', 'data-briven-auth-dropdown': 'open' },
676 [
677 h(
678 'button',
679 { type: 'button', onClick: handleProfile, class: 'briven-auth-userbutton-item' },
680 'profile',
681 ),
682 h(
683 'button',
684 { type: 'button', onClick: handleSignOut, class: 'briven-auth-userbutton-item' },
685 'sign out',
686 ),
687 ],
688 )
689 : null,
690 ],
691 );
692 };
693 },
694};
695
696// ─── UserProfile ───────────────────────────────────────────────────────────
697
698export interface UserProfileProps {
699 className?: string;
700 onUpdate?: () => void;
701}
702
703export const UserProfile = {
704 name: 'UserProfile',
705 props: {
706 className: { type: String, default: undefined },
707 onUpdate: { type: Function as PropType<() => void>, default: undefined },
708 },
709 setup(props: UserProfileProps) {
710 const auth = useBrivenAuth();
711 const { user, refresh } = useUser();
712
713 const name = ref('');
714 const currentPassword = ref('');
715 const newPassword = ref('');
716 const updatePending = ref(false);
717 const pwPending = ref(false);
718 const deletePending = ref(false);
719 const message = ref<string | null>(null);
720 const error = ref<string | null>(null);
721
722 watch(
723 () => user.value?.name,
724 (n) => { if (n) name.value = n; },
725 { immediate: true },
726 );
727
728 const handleUpdate = async (e: Event) => {
729 e.preventDefault();
730 updatePending.value = true;
731 error.value = null;
732 message.value = null;
733 const result = await auth.user.update({ name: name.value || undefined });
734 if (result.ok) {
735 message.value = 'profile updated';
736 await refresh();
737 props.onUpdate?.();
738 } else {
739 error.value = result.message;
740 }
741 updatePending.value = false;
742 };
743
744 const handleChangePassword = async (e: Event) => {
745 e.preventDefault();
746 pwPending.value = true;
747 error.value = null;
748 message.value = null;
749 const result = await auth.user.changePassword({ currentPassword: currentPassword.value, newPassword: newPassword.value });
750 if (result.ok) {
751 message.value = 'password changed';
752 currentPassword.value = '';
753 newPassword.value = '';
754 } else {
755 error.value = result.message;
756 }
757 pwPending.value = false;
758 };
759
760 const handleDelete = async () => {
761 if (typeof window !== 'undefined' && !window.confirm('Delete your account? This cannot be undone.')) return;
762 deletePending.value = true;
763 error.value = null;
764 message.value = null;
765 const result = await auth.user.delete();
766 if (result.ok) {
767 if (typeof window !== 'undefined') {
768 window.location.reload();
769 }
770 } else {
771 error.value = result.message;
772 deletePending.value = false;
773 }
774 };
775
776 return () => {
777 if (!user.value) {
778 return h('p', { class: 'briven-auth-message' }, 'not authenticated');
779 }
780 return h(
781 'div',
782 { class: props.className ?? 'briven-auth-userprofile', 'data-briven-auth': 'userprofile' },
783 [
784 h(
785 'form',
786 {
787 key: 'profile',
788 onSubmit: handleUpdate,
789 class: 'briven-auth-form',
790 'data-briven-auth-flow': 'profile-update',
791 },
792 [
793 h('h3', { class: 'briven-auth-heading' }, 'profile'),
794 h('input', {
795 key: 'name',
796 type: 'text',
797 placeholder: 'name',
798 value: name.value,
799 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
800 class: 'briven-auth-input',
801 }),
802 h('input', {
803 key: 'email',
804 type: 'email',
805 disabled: true,
806 value: user.value.email,
807 class: 'briven-auth-input',
808 }),
809 h(
810 'button',
811 {
812 key: 'submit',
813 type: 'submit',
814 disabled: updatePending.value,
815 class: 'briven-auth-submit',
816 },
817 updatePending.value ? 'saving…' : 'save profile',
818 ),
819 ],
820 ),
821 h(
822 'form',
823 {
824 key: 'password',
825 onSubmit: handleChangePassword,
826 class: 'briven-auth-form',
827 'data-briven-auth-flow': 'change-password',
828 },
829 [
830 h('h3', { class: 'briven-auth-heading' }, 'change password'),
831 h('input', {
832 key: 'current',
833 type: 'password',
834 required: true,
835 placeholder: 'current password',
836 value: currentPassword.value,
837 onInput: (e: Event) => { currentPassword.value = (e.target as HTMLInputElement).value; },
838 autocomplete: 'current-password',
839 class: 'briven-auth-input',
840 }),
841 h('input', {
842 key: 'new',
843 type: 'password',
844 required: true,
845 placeholder: 'new password',
846 value: newPassword.value,
847 onInput: (e: Event) => { newPassword.value = (e.target as HTMLInputElement).value; },
848 autocomplete: 'new-password',
849 class: 'briven-auth-input',
850 }),
851 h(
852 'button',
853 {
854 key: 'submit',
855 type: 'submit',
856 disabled: pwPending.value,
857 class: 'briven-auth-submit',
858 },
859 pwPending.value ? 'changing…' : 'change password',
860 ),
861 ],
862 ),
863 h(
864 'div',
865 { key: 'danger', class: 'briven-auth-danger-zone', 'data-briven-auth-flow': 'delete-account' },
866 [
867 h('h3', { class: 'briven-auth-heading' }, 'danger zone'),
868 h(
869 'button',
870 {
871 type: 'button',
872 onClick: handleDelete,
873 disabled: deletePending.value,
874 class: 'briven-auth-danger-button',
875 },
876 deletePending.value ? 'deleting…' : 'delete account',
877 ),
878 ],
879 ),
880 message.value ? h('p', { key: 'message', class: 'briven-auth-message' }, message.value) : null,
881 error.value ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value) : null,
882 ],
883 );
884 };
885 },
886};
887
888// ─── SessionManager ────────────────────────────────────────────────────────
889
890export interface SessionManagerProps {
891 className?: string;
892}
893
894export const SessionManager = {
895 name: 'SessionManager',
896 props: {
897 className: { type: String, default: undefined },
898 },
899 setup(props: SessionManagerProps) {
900 const auth = useBrivenAuth();
901 const sessions = ref<ClientSession[]>([]);
902 const isLoading = ref(true);
903 const error = ref<string | null>(null);
904
905 const load = async () => {
906 isLoading.value = true;
907 error.value = null;
908 const result = await auth.sessions.list();
909 if (result.ok) {
910 sessions.value = result.sessions;
911 } else {
912 error.value = result.message;
913 }
914 isLoading.value = false;
915 };
916
917 onMounted(() => {
918 void load();
919 });
920
921 const handleRevoke = async (sessionId: string) => {
922 const result = await auth.sessions.revoke(sessionId);
923 if (result.ok) {
924 await load();
925 } else {
926 error.value = result.message;
927 }
928 };
929
930 return () =>
931 h(
932 'div',
933 { class: props.className ?? 'briven-auth-sessionmanager', 'data-briven-auth': 'sessionmanager' },
934 [
935 h('h3', { class: 'briven-auth-heading' }, 'active sessions'),
936 isLoading.value
937 ? h('p', { class: 'briven-auth-message' }, 'loading…')
938 : sessions.value.length === 0
939 ? h('p', { class: 'briven-auth-message' }, 'no active sessions')
940 : h(
941 'ul',
942 { class: 'briven-auth-session-list' },
943 sessions.value.map((s) =>
944 h(
945 'li',
946 { key: s.id, class: 'briven-auth-session-item' },
947 [
948 h('span', { class: 'briven-auth-session-info' }, s.userAgent ?? 'unknown device'),
949 h(
950 'button',
951 {
952 type: 'button',
953 onClick: () => handleRevoke(s.id),
954 class: 'briven-auth-session-revoke',
955 },
956 'revoke',
957 ),
958 ],
959 ),
960 ),
961 ),
962 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
963 ],
964 );
965 },
966};
967
968// ─── OrganizationSwitcher ─────────────────────────────────────────────────
969
970export interface OrganizationSwitcherProps {
971 className?: string;
972}
973
974export const OrganizationSwitcher = {
975 name: 'OrganizationSwitcher',
976 props: {
977 className: { type: String, default: undefined },
978 },
979 setup(props: OrganizationSwitcherProps) {
980 const auth = useBrivenAuth();
981 const { activeOrg, setActive } = useActiveOrganization();
982 const orgs = ref<Org[]>([]);
983 const isLoading = ref(true);
984 const open = ref(false);
985 const showCreate = ref(false);
986
987 const load = async () => {
988 isLoading.value = true;
989 const result = await auth.organization.list();
990 if (result.ok) orgs.value = result.data;
991 isLoading.value = false;
992 };
993
994 onMounted(() => {
995 void load();
996 });
997
998 const handleCreate = async (name: string, slug: string) => {
999 const result = await auth.organization.create({ name, slug });
1000 if (result.ok) {
1001 showCreate.value = false;
1002 await load();
1003 }
1004 return result;
1005 };
1006
1007 const handleSwitch = async (orgId: string) => {
1008 await setActive(orgId);
1009 open.value = false;
1010 };
1011
1012 return () => {
1013 if (isLoading.value) return null;
1014
1015 if (orgs.value.length === 0) {
1016 return h(
1017 'button',
1018 { type: 'button', onClick: () => { showCreate.value = true; }, class: props.className ?? 'briven-auth-org-switcher' },
1019 'create organization',
1020 );
1021 }
1022 return h(
1023 'div',
1024 { class: props.className ?? 'briven-auth-org-switcher', 'data-briven-auth': 'org-switcher' },
1025 [
1026 h(
1027 'button',
1028 { type: 'button', onClick: () => { open.value = !open.value; }, class: 'briven-auth-org-switcher-trigger' },
1029 activeOrg.value?.name ?? 'switch organization',
1030 ),
1031 open.value
1032 ? h(
1033 'div',
1034 { class: 'briven-auth-org-switcher-dropdown' },
1035 [
1036 ...orgs.value.map((org) =>
1037 h(
1038 'button',
1039 {
1040 key: org.id,
1041 type: 'button',
1042 onClick: () => handleSwitch(org.id),
1043 class:
1044 org.id === activeOrg.value?.id
1045 ? 'briven-auth-org-switcher-item briven-auth-org-switcher-item-active'
1046 : 'briven-auth-org-switcher-item',
1047 },
1048 org.name,
1049 ),
1050 ),
1051 h(
1052 'button',
1053 { type: 'button', onClick: () => { showCreate.value = true; }, class: 'briven-auth-org-switcher-create' },
1054 '+ create organization',
1055 ),
1056 ],
1057 )
1058 : null,
1059 showCreate.value
1060 ? h(CreateOrganization, {
1061 key: 'create',
1062 onCreate: handleCreate,
1063 onCancel: () => { showCreate.value = false; },
1064 })
1065 : null,
1066 ],
1067 );
1068 };
1069 },
1070};
1071
1072// ─── CreateOrganization ───────────────────────────────────────────────────
1073
1074export interface CreateOrganizationProps {
1075 onCreate(name: string, slug: string): Promise<unknown>;
1076 onCancel(): void;
1077}
1078
1079export const CreateOrganization = {
1080 name: 'CreateOrganization',
1081 props: {
1082 onCreate: { type: Function as PropType<(name: string, slug: string) => Promise<unknown>>, required: true },
1083 onCancel: { type: Function as PropType<() => void>, required: true },
1084 },
1085 setup(props: CreateOrganizationProps) {
1086 const name = ref('');
1087 const slug = ref('');
1088 const pending = ref(false);
1089 const error = ref<string | null>(null);
1090
1091 const handleSubmit = async (e: Event) => {
1092 e.preventDefault();
1093 pending.value = true;
1094 error.value = null;
1095 const result = await props.onCreate(name.value, slug.value);
1096 if (result && typeof result === 'object' && 'ok' in result && !result.ok) {
1097 error.value = (result as { message?: string }).message ?? 'create failed';
1098 }
1099 pending.value = false;
1100 };
1101
1102 return () =>
1103 h(
1104 'div',
1105 { class: 'briven-auth-create-org' },
1106 [
1107 h('h3', { class: 'briven-auth-heading' }, 'create organization'),
1108 h(
1109 'form',
1110 { class: 'briven-auth-form', onSubmit: handleSubmit },
1111 [
1112 h('input', {
1113 type: 'text',
1114 required: true,
1115 placeholder: 'organization name',
1116 value: name.value,
1117 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
1118 class: 'briven-auth-input',
1119 }),
1120 h('input', {
1121 type: 'text',
1122 required: true,
1123 placeholder: 'slug (lowercase-hyphens)',
1124 value: slug.value,
1125 onInput: (e: Event) => { slug.value = (e.target as HTMLInputElement).value; },
1126 pattern: '[a-z0-9-]{1,64}',
1127 class: 'briven-auth-input',
1128 }),
1129 h(
1130 'button',
1131 { type: 'submit', disabled: pending.value, class: 'briven-auth-submit' },
1132 pending.value ? 'creating…' : 'create',
1133 ),
1134 ],
1135 ),
1136 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1137 h(
1138 'button',
1139 { type: 'button', onClick: props.onCancel, class: 'briven-auth-cancel' },
1140 'cancel',
1141 ),
1142 ],
1143 );
1144 },
1145};
1146
1147// ─── OrganizationProfile ──────────────────────────────────────────────────
1148
1149export interface OrganizationProfileProps {
1150 orgId: string;
1151 className?: string;
1152}
1153
1154export const OrganizationProfile = {
1155 name: 'OrganizationProfile',
1156 props: {
1157 orgId: { type: String, required: true },
1158 className: { type: String, default: undefined },
1159 },
1160 setup(props: OrganizationProfileProps) {
1161 const auth = useBrivenAuth();
1162 const members = ref<OrgMember[]>([]);
1163 const invites = ref<OrgInvite[]>([]);
1164 const inviteEmail = ref('');
1165 const isLoading = ref(true);
1166 const error = ref<string | null>(null);
1167
1168 const load = async () => {
1169 isLoading.value = true;
1170 const [mResult, iResult] = await Promise.all([
1171 auth.organization.listMembers(props.orgId),
1172 auth.organization.listInvites(props.orgId),
1173 ]);
1174 if (mResult.ok) members.value = mResult.data;
1175 if (iResult.ok) invites.value = iResult.data;
1176 isLoading.value = false;
1177 };
1178
1179 onMounted(() => {
1180 void load();
1181 });
1182
1183 const handleInvite = async (e: Event) => {
1184 e.preventDefault();
1185 error.value = null;
1186 const result = await auth.organization.createInvite(props.orgId, { email: inviteEmail.value });
1187 if (result.ok) {
1188 inviteEmail.value = '';
1189 await load();
1190 } else {
1191 error.value = result.message;
1192 }
1193 };
1194
1195 const handleRemove = async (userId: string) => {
1196 const result = await auth.organization.removeMember(props.orgId, userId);
1197 if (result.ok) await load();
1198 else error.value = result.message;
1199 };
1200
1201 return () =>
1202 h(
1203 'div',
1204 { class: props.className ?? 'briven-auth-org-profile', 'data-briven-auth': 'org-profile' },
1205 [
1206 h('h3', { class: 'briven-auth-heading' }, 'members'),
1207 isLoading.value
1208 ? h('p', { class: 'briven-auth-message' }, 'loading…')
1209 : h(
1210 'ul',
1211 { class: 'briven-auth-member-list' },
1212 members.value.map((m) =>
1213 h(
1214 'li',
1215 { key: m.id, class: 'briven-auth-member-item' },
1216 [
1217 h('span', { class: 'briven-auth-member-role' }, m.role),
1218 h('span', { class: 'briven-auth-member-id' }, m.userId),
1219 m.role !== 'owner'
1220 ? h(
1221 'button',
1222 {
1223 type: 'button',
1224 onClick: () => handleRemove(m.userId),
1225 class: 'briven-auth-member-remove',
1226 },
1227 'remove',
1228 )
1229 : null,
1230 ],
1231 ),
1232 ),
1233 ),
1234 h('h3', { class: 'briven-auth-heading' }, 'invites'),
1235 h(
1236 'form',
1237 { class: 'briven-auth-form', onSubmit: handleInvite },
1238 [
1239 h('input', {
1240 type: 'email',
1241 required: true,
1242 placeholder: 'email to invite',
1243 value: inviteEmail.value,
1244 onInput: (e: Event) => { inviteEmail.value = (e.target as HTMLInputElement).value; },
1245 class: 'briven-auth-input',
1246 }),
1247 h('button', { type: 'submit', class: 'briven-auth-submit' }, 'send invite'),
1248 ],
1249 ),
1250 invites.value.length > 0
1251 ? h(
1252 'ul',
1253 { class: 'briven-auth-invite-list' },
1254 invites.value.map((i) => h('li', { key: i.id, class: 'briven-auth-invite-item' }, [`${i.email} · ${i.role}`])),
1255 )
1256 : null,
1257 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1258 ],
1259 );
1260 },
1261};
1262
1263// ─── TwoFactorSetup ───────────────────────────────────────────────────────
1264
1265export interface TwoFactorSetupProps {
1266 className?: string;
1267 onEnabled?: () => void;
1268}
1269
1270export const TwoFactorSetup = {
1271 name: 'TwoFactorSetup',
1272 props: {
1273 className: { type: String, default: undefined },
1274 onEnabled: { type: Function as PropType<() => void>, default: undefined },
1275 },
1276 setup(props: TwoFactorSetupProps) {
1277 const auth = useBrivenAuth();
1278 const step = ref<'idle' | 'enabling' | 'verify'>('idle');
1279 const code = ref('');
1280 const backupCodes = ref<string[]>([]);
1281 const error = ref<string | null>(null);
1282
1283 const handleEnable = async () => {
1284 step.value = 'enabling';
1285 error.value = null;
1286 const result = await auth.twoFactor.enable();
1287 if (result.ok) {
1288 step.value = 'verify';
1289 } else {
1290 error.value = result.message;
1291 step.value = 'idle';
1292 }
1293 };
1294
1295 const handleVerify = async (e: Event) => {
1296 e.preventDefault();
1297 error.value = null;
1298 const result = await auth.twoFactor.verify(code.value);
1299 if (result.ok) {
1300 const codes = await auth.twoFactor.generateBackupCodes();
1301 if (codes.ok) backupCodes.value = codes.codes;
1302 props.onEnabled?.();
1303 } else {
1304 error.value = result.message;
1305 }
1306 };
1307
1308 return () =>
1309 h(
1310 'div',
1311 { class: props.className ?? 'briven-auth-2fa-setup' },
1312 [
1313 step.value === 'idle'
1314 ? h(
1315 'button',
1316 { type: 'button', onClick: handleEnable, class: 'briven-auth-submit' },
1317 'enable two-factor',
1318 )
1319 : null,
1320 step.value === 'verify'
1321 ? h(
1322 'form',
1323 { onSubmit: handleVerify, class: 'briven-auth-form' },
1324 [
1325 h('p', { class: 'briven-auth-message' }, 'enter the 6-digit code from your authenticator app'),
1326 h('input', {
1327 type: 'text',
1328 required: true,
1329 placeholder: '6-digit code',
1330 value: code.value,
1331 onInput: (e: Event) => { code.value = (e.target as HTMLInputElement).value; },
1332 pattern: '\\d{6}',
1333 maxlength: 6,
1334 class: 'briven-auth-input',
1335 }),
1336 h('button', { type: 'submit', class: 'briven-auth-submit' }, 'verify'),
1337 ],
1338 )
1339 : null,
1340 backupCodes.value.length > 0
1341 ? h(
1342 'div',
1343 { class: 'briven-auth-backup-codes' },
1344 [
1345 h('p', { class: 'briven-auth-message' }, 'save these backup codes:'),
1346 h('ul', {}, backupCodes.value.map((c) => h('li', { key: c, class: 'briven-auth-code' }, c))),
1347 ],
1348 )
1349 : null,
1350 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1351 ],
1352 );
1353 },
1354};
1355
1356// ─── PasskeyButton ────────────────────────────────────────────────────────
1357
1358export interface PasskeyButtonProps {
1359 className?: string;
1360 mode?: 'register' | 'sign-in';
1361}
1362
1363export const PasskeyButton = {
1364 name: 'PasskeyButton',
1365 props: {
1366 className: { type: String, default: undefined },
1367 mode: { type: String as PropType<'register' | 'sign-in'>, default: 'register' },
1368 },
1369 setup(props: PasskeyButtonProps) {
1370 const auth = useBrivenAuth();
1371 const pending = ref(false);
1372 const error = ref<string | null>(null);
1373
1374 const handleClick = async () => {
1375 pending.value = true;
1376 error.value = null;
1377 if (props.mode === 'register') {
1378 const result = await auth.passkey.register();
1379 if (!result.ok) error.value = result.message;
1380 } else {
1381 const result = await auth.passkey.signIn();
1382 if (!result.ok) error.value = result.message;
1383 }
1384 pending.value = false;
1385 };
1386
1387 return () =>
1388 h(
1389 'div',
1390 { class: props.className ?? 'briven-auth-passkey' },
1391 [
1392 h(
1393 'button',
1394 {
1395 type: 'button',
1396 onClick: handleClick,
1397 disabled: pending.value,
1398 class: 'briven-auth-passkey-button',
1399 },
1400 props.mode === 'register' ? 'register passkey' : 'sign in with passkey',
1401 ),
1402 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1403 ],
1404 );
1405 },
1406};
1407
1408export type {
1409 BrivenAuthClient,
1410 ClientSession,
1411 MembershipRequest,
1412 OAuthProvider,
1413 Org,
1414 OrgDomain,
1415 OrgInvite,
1416 OrgMember,
1417 OrgPermission,
1418 OrgRole,
1419 Passkey,
1420 SessionResponse,
1421 SignInResult,
1422 SimpleResult,
1423 SsoConnection,
1424 SsoProviderType,
1425 User,
1426 UserEmail,
1427};