index.ts265 lines · main
1/**
2 * @briven/auth/svelte — Svelte bindings for `@briven/auth`.
3 *
4 * import { createBrivenAuth } from '@briven/auth';
5 * import { setBrivenAuthContext, createSessionStore, createUserStore } from '@briven/auth/svelte';
6 *
7 * const auth = createBrivenAuth({ projectId: 'p_abc123', publicKey: '...' });
8 *
9 * <!-- App.svelte -->
10 * <script>
11 * import { setBrivenAuthContext } from '@briven/auth/svelte';
12 * setBrivenAuthContext(auth);
13 * </script>
14 *
15 * <!-- Child.svelte -->
16 * <script>
17 * import { createSessionStore } from '@briven/auth/svelte';
18 * const { session, isLoading, refresh } = createSessionStore();
19 * </script>
20 *
21 * Zero hard dependency on SvelteKit — works in any Svelte 4/5 environment.
22 */
23
24import { getContext, setContext } from 'svelte';
25import { writable, type Readable } from 'svelte/store';
26
27import {
28 type BrivenAuthClient,
29 type ClientSession,
30 type Org,
31 type SessionResponse,
32 type SsoConnection,
33 type User,
34 type UserEmail,
35} from '../index.js';
36
37const BRIVEN_AUTH_KEY = Symbol('briven-auth');
38
39/** Set the auth client in Svelte context (call once in your root layout). */
40export function setBrivenAuthContext(client: BrivenAuthClient): void {
41 setContext(BRIVEN_AUTH_KEY, client);
42}
43
44/** Get the auth client from Svelte context. Throws if not set. */
45export function getBrivenAuthContext(): BrivenAuthClient {
46 const client = getContext<BrivenAuthClient | undefined>(BRIVEN_AUTH_KEY);
47 if (!client) {
48 throw new Error('getBrivenAuthContext must be called inside a component with setBrivenAuthContext');
49 }
50 return client;
51}
52
53// ─── Stores ────────────────────────────────────────────────────────────────
54
55export interface SessionStore {
56 session: Readable<SessionResponse | null>;
57 isLoading: Readable<boolean>;
58 refresh: () => Promise<void>;
59}
60
61export function createSessionStore(client?: BrivenAuthClient): SessionStore {
62 const auth = client ?? getBrivenAuthContext();
63 const session = writable<SessionResponse | null>(null);
64 const isLoading = writable(true);
65
66 const refresh = async () => {
67 isLoading.set(true);
68 const next = await auth.getSession();
69 session.set(next);
70 isLoading.set(false);
71 };
72
73 // Auto-fetch on first subscription
74 const unsub = session.subscribe(() => {});
75 refresh().then(() => unsub());
76
77 return { session, isLoading, refresh };
78}
79
80export interface UserStore {
81 user: Readable<User | null>;
82 isLoading: Readable<boolean>;
83 refresh: () => Promise<void>;
84}
85
86export function createUserStore(client?: BrivenAuthClient): UserStore {
87 const auth = client ?? getBrivenAuthContext();
88 const user = writable<User | null>(null);
89 const isLoading = writable(true);
90
91 const refresh = async () => {
92 isLoading.set(true);
93 const next = await auth.getUser();
94 user.set(next);
95 isLoading.set(false);
96 };
97
98 const unsub = user.subscribe(() => {});
99 refresh().then(() => unsub());
100
101 return { user, isLoading, refresh };
102}
103
104export interface UserMetadataStore {
105 metadata: Readable<Record<string, unknown> | null>;
106 isLoading: Readable<boolean>;
107 refresh: () => Promise<void>;
108 set: (patch: Record<string, unknown>) => Promise<void>;
109}
110
111export function createUserMetadataStore(client?: BrivenAuthClient): UserMetadataStore {
112 const auth = client ?? getBrivenAuthContext();
113 const metadata = writable<Record<string, unknown> | null>(null);
114 const isLoading = writable(true);
115
116 const refresh = async () => {
117 isLoading.set(true);
118 const result = await auth.user.getMetadata();
119 metadata.set(result.ok ? result.publicMetadata : null);
120 isLoading.set(false);
121 };
122
123 const set = async (patch: Record<string, unknown>) => {
124 const result = await auth.user.setMetadata(patch);
125 if (result.ok) metadata.set(result.publicMetadata);
126 };
127
128 const unsub = metadata.subscribe(() => {});
129 refresh().then(() => unsub());
130
131 return { metadata, isLoading, refresh, set };
132}
133
134export interface UserEmailsStore {
135 emails: Readable<UserEmail[] | null>;
136 isLoading: Readable<boolean>;
137 refresh: () => Promise<void>;
138 add: (email: string) => Promise<void>;
139 remove: (emailId: string) => Promise<void>;
140}
141
142export function createUserEmailsStore(client?: BrivenAuthClient): UserEmailsStore {
143 const auth = client ?? getBrivenAuthContext();
144 const emails = writable<UserEmail[] | null>(null);
145 const isLoading = writable(true);
146
147 const refresh = async () => {
148 isLoading.set(true);
149 const result = await auth.user.listEmails();
150 emails.set(result.ok ? result.emails : null);
151 isLoading.set(false);
152 };
153
154 const add = async (email: string) => {
155 const result = await auth.user.addEmail(email);
156 if (result.ok) await refresh();
157 };
158
159 const remove = async (emailId: string) => {
160 const result = await auth.user.removeEmail(emailId);
161 if (result.ok) await refresh();
162 };
163
164 const unsub = emails.subscribe(() => {});
165 refresh().then(() => unsub());
166
167 return { emails, isLoading, refresh, add, remove };
168}
169
170export interface ActiveOrganizationStore {
171 activeOrg: Readable<Org | null>;
172 isLoading: Readable<boolean>;
173 refresh: () => Promise<void>;
174 setActive: (orgId: string) => Promise<void>;
175}
176
177export function createActiveOrganizationStore(client?: BrivenAuthClient): ActiveOrganizationStore {
178 const auth = client ?? getBrivenAuthContext();
179 const activeOrg = writable<Org | null>(null);
180 const isLoading = writable(true);
181
182 const refresh = async () => {
183 isLoading.set(true);
184 const result = await auth.organization.getActive();
185 if (result.ok) activeOrg.set(result.data);
186 isLoading.set(false);
187 };
188
189 const setActive = async (orgId: string) => {
190 const result = await auth.organization.setActive(orgId);
191 if (result.ok) await refresh();
192 };
193
194 const unsub = activeOrg.subscribe(() => {});
195 refresh().then(() => unsub());
196
197 return { activeOrg, isLoading, refresh, setActive };
198}
199
200export interface SessionsStore {
201 sessions: Readable<ClientSession[]>;
202 isLoading: Readable<boolean>;
203 error: Readable<string | null>;
204 refresh: () => Promise<void>;
205 revoke: (sessionId: string) => Promise<void>;
206}
207
208export function createSessionsStore(client?: BrivenAuthClient): SessionsStore {
209 const auth = client ?? getBrivenAuthContext();
210 const sessions = writable<ClientSession[]>([]);
211 const isLoading = writable(true);
212 const error = writable<string | null>(null);
213
214 const refresh = async () => {
215 isLoading.set(true);
216 error.set(null);
217 const result = await auth.sessions.list();
218 if (result.ok) {
219 sessions.set(result.sessions);
220 } else {
221 error.set(result.message);
222 }
223 isLoading.set(false);
224 };
225
226 const revoke = async (sessionId: string) => {
227 const result = await auth.sessions.revoke(sessionId);
228 if (result.ok) {
229 await refresh();
230 } else {
231 error.set(result.message);
232 }
233 };
234
235 const unsub = sessions.subscribe(() => {});
236 refresh().then(() => unsub());
237
238 return { sessions, isLoading, error, refresh, revoke };
239}
240
241// ─── SSO Store ─────────────────────────────────────────────────────────────
242
243export interface SsoStore {
244 connections: Readable<Array<Pick<SsoConnection, 'id' | 'name' | 'providerType' | 'domains'>> | null>;
245 isLoading: Readable<boolean>;
246 refresh: () => Promise<void>;
247}
248
249export function createSsoStore(client?: BrivenAuthClient): SsoStore {
250 const auth = client ?? getBrivenAuthContext();
251 const connections = writable<Array<Pick<SsoConnection, 'id' | 'name' | 'providerType' | 'domains'>> | null>(null);
252 const isLoading = writable(true);
253
254 const refresh = async () => {
255 isLoading.set(true);
256 const result = await auth.sso.listConnections();
257 if (result.ok) connections.set(result.connections);
258 isLoading.set(false);
259 };
260
261 const unsub = connections.subscribe(() => {});
262 refresh().then(() => unsub());
263
264 return { connections, isLoading, refresh };
265}