@korajs/auth 1.0.0-beta.11 → 1.0.0-beta.13

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2852 -1675
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +779 -168
  17. package/dist/server.d.ts +779 -168
  18. package/dist/server.js +2831 -1665
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +328 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +322 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +272 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +334 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,401 @@
1
+ import { KoraError } from '@korajs/core'
2
+
3
+ // ============================================================================
4
+ // Session Types
5
+ // ============================================================================
6
+
7
+ /**
8
+ * A user session with metadata.
9
+ */
10
+ export interface Session {
11
+ /** Unique session ID */
12
+ id: string
13
+ /** User ID */
14
+ userId: string
15
+ /** Device ID (if available) */
16
+ deviceId: string | null
17
+ /** IP address of the client (for display, not for security decisions) */
18
+ ipAddress: string | null
19
+ /** User agent string */
20
+ userAgent: string | null
21
+ /** When the session was created */
22
+ createdAt: number
23
+ /** When the session was last active */
24
+ lastActiveAt: number
25
+ /** When the session expires (absolute expiry) */
26
+ expiresAt: number
27
+ /** Whether MFA has been completed for this session */
28
+ mfaVerified: boolean
29
+ /** Custom metadata */
30
+ metadata?: Record<string, unknown>
31
+ }
32
+
33
+ /**
34
+ * Configuration for the session manager.
35
+ */
36
+ export interface SessionManagerConfig {
37
+ /** Session store implementation */
38
+ store: SessionStore
39
+ /** Session TTL in milliseconds. Default: 7 days */
40
+ sessionTtlMs?: number
41
+ /** Idle timeout in milliseconds. Default: 30 minutes */
42
+ idleTimeoutMs?: number
43
+ /** Maximum concurrent sessions per user. Default: 10 */
44
+ maxSessionsPerUser?: number
45
+ /** Whether to extend session on activity. Default: true */
46
+ slidingWindow?: boolean
47
+ }
48
+
49
+ /**
50
+ * Parameters for creating a new session.
51
+ */
52
+ export interface CreateSessionParams {
53
+ userId: string
54
+ deviceId?: string
55
+ ipAddress?: string
56
+ userAgent?: string
57
+ mfaVerified?: boolean
58
+ metadata?: Record<string, unknown>
59
+ }
60
+
61
+ /**
62
+ * Store for session data.
63
+ */
64
+ export interface SessionStore {
65
+ /** Create a new session */
66
+ create(session: Session): Promise<void>
67
+ /** Get a session by ID */
68
+ getById(sessionId: string): Promise<Session | null>
69
+ /** Update a session */
70
+ update(session: Session): Promise<void>
71
+ /** Delete a session */
72
+ delete(sessionId: string): Promise<void>
73
+ /** Get all sessions for a user */
74
+ listByUserId(userId: string): Promise<Session[]>
75
+ /** Delete all sessions for a user */
76
+ deleteAllForUser(userId: string): Promise<number>
77
+ /** Delete all sessions for a user except one */
78
+ deleteAllExcept(userId: string, keepSessionId: string): Promise<number>
79
+ /** Clean up expired sessions */
80
+ cleanExpired(): Promise<number>
81
+ }
82
+
83
+ // ============================================================================
84
+ // Errors
85
+ // ============================================================================
86
+
87
+ export class SessionError extends KoraError {
88
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
89
+ super(message, code, context)
90
+ this.name = 'SessionError'
91
+ }
92
+ }
93
+
94
+ export class SessionNotFoundError extends SessionError {
95
+ constructor(sessionId: string) {
96
+ super('Session not found or expired.', 'SESSION_NOT_FOUND', { sessionId })
97
+ }
98
+ }
99
+
100
+ export class SessionExpiredError extends SessionError {
101
+ constructor(sessionId: string) {
102
+ super('Session has expired.', 'SESSION_EXPIRED', { sessionId })
103
+ }
104
+ }
105
+
106
+ export class SessionLimitExceededError extends SessionError {
107
+ constructor(userId: string, limit: number) {
108
+ super(
109
+ `Maximum concurrent sessions (${limit}) reached. Revoke an existing session first.`,
110
+ 'SESSION_LIMIT_EXCEEDED',
111
+ { userId, limit },
112
+ )
113
+ }
114
+ }
115
+
116
+ export class SessionMfaRequiredError extends SessionError {
117
+ constructor(sessionId: string) {
118
+ super('MFA verification is required for this session.', 'SESSION_MFA_REQUIRED', { sessionId })
119
+ }
120
+ }
121
+
122
+ // ============================================================================
123
+ // InMemorySessionStore
124
+ // ============================================================================
125
+
126
+ /**
127
+ * In-memory session store for development and testing.
128
+ */
129
+ export class InMemorySessionStore implements SessionStore {
130
+ private readonly sessions = new Map<string, Session>()
131
+
132
+ async create(session: Session): Promise<void> {
133
+ this.sessions.set(session.id, { ...session })
134
+ }
135
+
136
+ async getById(sessionId: string): Promise<Session | null> {
137
+ const session = this.sessions.get(sessionId)
138
+ return session ? { ...session } : null
139
+ }
140
+
141
+ async update(session: Session): Promise<void> {
142
+ this.sessions.set(session.id, { ...session })
143
+ }
144
+
145
+ async delete(sessionId: string): Promise<void> {
146
+ this.sessions.delete(sessionId)
147
+ }
148
+
149
+ async listByUserId(userId: string): Promise<Session[]> {
150
+ const results: Session[] = []
151
+ for (const session of this.sessions.values()) {
152
+ if (session.userId === userId) {
153
+ results.push({ ...session })
154
+ }
155
+ }
156
+ return results.sort((a, b) => b.lastActiveAt - a.lastActiveAt)
157
+ }
158
+
159
+ async deleteAllForUser(userId: string): Promise<number> {
160
+ let count = 0
161
+ for (const [id, session] of this.sessions) {
162
+ if (session.userId === userId) {
163
+ this.sessions.delete(id)
164
+ count++
165
+ }
166
+ }
167
+ return count
168
+ }
169
+
170
+ async deleteAllExcept(userId: string, keepSessionId: string): Promise<number> {
171
+ let count = 0
172
+ for (const [id, session] of this.sessions) {
173
+ if (session.userId === userId && id !== keepSessionId) {
174
+ this.sessions.delete(id)
175
+ count++
176
+ }
177
+ }
178
+ return count
179
+ }
180
+
181
+ async cleanExpired(): Promise<number> {
182
+ const now = Date.now()
183
+ let count = 0
184
+ for (const [id, session] of this.sessions) {
185
+ if (now > session.expiresAt) {
186
+ this.sessions.delete(id)
187
+ count++
188
+ }
189
+ }
190
+ return count
191
+ }
192
+ }
193
+
194
+ // ============================================================================
195
+ // SessionManager
196
+ // ============================================================================
197
+
198
+ const DEFAULT_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
199
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
200
+ const DEFAULT_MAX_SESSIONS = 10
201
+
202
+ /**
203
+ * Manages user sessions with support for:
204
+ * - Session creation, validation, and revocation
205
+ * - Sliding window expiry (extends on activity)
206
+ * - Idle timeout detection
207
+ * - Max concurrent sessions per user
208
+ * - MFA verification tracking
209
+ * - "Sign out everywhere" support
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const sessions = new SessionManager({
214
+ * store: new InMemorySessionStore(),
215
+ * sessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
216
+ * idleTimeoutMs: 30 * 60 * 1000, // 30 minutes
217
+ * maxSessionsPerUser: 5,
218
+ * })
219
+ *
220
+ * // Create session on login
221
+ * const session = await sessions.create({
222
+ * userId: 'user-123',
223
+ * ipAddress: '192.168.1.1',
224
+ * userAgent: 'Mozilla/5.0...',
225
+ * })
226
+ *
227
+ * // Validate session on each request
228
+ * const valid = await sessions.validate(session.id)
229
+ *
230
+ * // Touch session to extend idle timeout
231
+ * await sessions.touch(session.id)
232
+ * ```
233
+ */
234
+ export class SessionManager {
235
+ private readonly store: SessionStore
236
+ private readonly sessionTtlMs: number
237
+ private readonly idleTimeoutMs: number
238
+ private readonly maxSessionsPerUser: number
239
+ private readonly slidingWindow: boolean
240
+
241
+ constructor(config: SessionManagerConfig) {
242
+ this.store = config.store
243
+ this.sessionTtlMs = config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS
244
+ this.idleTimeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
245
+ this.maxSessionsPerUser = config.maxSessionsPerUser ?? DEFAULT_MAX_SESSIONS
246
+ this.slidingWindow = config.slidingWindow ?? true
247
+ }
248
+
249
+ /**
250
+ * Create a new session for a user.
251
+ * Enforces the maximum concurrent sessions limit.
252
+ */
253
+ async create(params: CreateSessionParams): Promise<Session> {
254
+ // Enforce max sessions
255
+ const existing = await this.store.listByUserId(params.userId)
256
+ const activeSessions = existing.filter((s) => Date.now() <= s.expiresAt)
257
+
258
+ if (activeSessions.length >= this.maxSessionsPerUser) {
259
+ throw new SessionLimitExceededError(params.userId, this.maxSessionsPerUser)
260
+ }
261
+
262
+ const now = Date.now()
263
+ const session: Session = {
264
+ id: generateSessionId(),
265
+ userId: params.userId,
266
+ deviceId: params.deviceId ?? null,
267
+ ipAddress: params.ipAddress ?? null,
268
+ userAgent: params.userAgent ?? null,
269
+ createdAt: now,
270
+ lastActiveAt: now,
271
+ expiresAt: now + this.sessionTtlMs,
272
+ mfaVerified: params.mfaVerified ?? false,
273
+ metadata: params.metadata,
274
+ }
275
+
276
+ await this.store.create(session)
277
+ return session
278
+ }
279
+
280
+ /**
281
+ * Validate a session.
282
+ * Returns the session if valid, throws if not found or expired.
283
+ */
284
+ async validate(sessionId: string): Promise<Session> {
285
+ const session = await this.store.getById(sessionId)
286
+ if (!session) {
287
+ throw new SessionNotFoundError(sessionId)
288
+ }
289
+
290
+ const now = Date.now()
291
+
292
+ // Check absolute expiry
293
+ if (now > session.expiresAt) {
294
+ await this.store.delete(sessionId)
295
+ throw new SessionExpiredError(sessionId)
296
+ }
297
+
298
+ // Check idle timeout
299
+ if (now - session.lastActiveAt > this.idleTimeoutMs) {
300
+ await this.store.delete(sessionId)
301
+ throw new SessionExpiredError(sessionId)
302
+ }
303
+
304
+ return session
305
+ }
306
+
307
+ /**
308
+ * Touch a session to update its last activity time.
309
+ * If sliding window is enabled, also extends the absolute expiry.
310
+ */
311
+ async touch(sessionId: string): Promise<Session> {
312
+ const session = await this.validate(sessionId)
313
+ const now = Date.now()
314
+
315
+ session.lastActiveAt = now
316
+
317
+ if (this.slidingWindow) {
318
+ session.expiresAt = now + this.sessionTtlMs
319
+ }
320
+
321
+ await this.store.update(session)
322
+ return session
323
+ }
324
+
325
+ /**
326
+ * Mark a session as MFA-verified.
327
+ */
328
+ async markMfaVerified(sessionId: string): Promise<Session> {
329
+ const session = await this.validate(sessionId)
330
+ session.mfaVerified = true
331
+ await this.store.update(session)
332
+ return session
333
+ }
334
+
335
+ /**
336
+ * Require MFA verification on a session.
337
+ * Throws SessionMfaRequiredError if MFA is not verified.
338
+ */
339
+ async requireMfa(sessionId: string): Promise<Session> {
340
+ const session = await this.validate(sessionId)
341
+ if (!session.mfaVerified) {
342
+ throw new SessionMfaRequiredError(sessionId)
343
+ }
344
+ return session
345
+ }
346
+
347
+ /**
348
+ * Revoke (delete) a session.
349
+ */
350
+ async revoke(sessionId: string): Promise<void> {
351
+ await this.store.delete(sessionId)
352
+ }
353
+
354
+ /**
355
+ * Revoke all sessions for a user (sign out everywhere).
356
+ * Returns the number of sessions revoked.
357
+ */
358
+ async revokeAll(userId: string): Promise<number> {
359
+ return this.store.deleteAllForUser(userId)
360
+ }
361
+
362
+ /**
363
+ * Revoke all sessions for a user except the current one.
364
+ * Returns the number of sessions revoked.
365
+ */
366
+ async revokeOthers(userId: string, currentSessionId: string): Promise<number> {
367
+ return this.store.deleteAllExcept(userId, currentSessionId)
368
+ }
369
+
370
+ /**
371
+ * List all active sessions for a user.
372
+ */
373
+ async listSessions(userId: string): Promise<Session[]> {
374
+ const sessions = await this.store.listByUserId(userId)
375
+ const now = Date.now()
376
+ // Filter out expired sessions
377
+ return sessions.filter((s) => now <= s.expiresAt && now - s.lastActiveAt <= this.idleTimeoutMs)
378
+ }
379
+
380
+ /**
381
+ * Clean up expired sessions.
382
+ * Returns the number of sessions cleaned.
383
+ */
384
+ async cleanExpired(): Promise<number> {
385
+ return this.store.cleanExpired()
386
+ }
387
+ }
388
+
389
+ // ============================================================================
390
+ // Helpers
391
+ // ============================================================================
392
+
393
+ function generateSessionId(): string {
394
+ const bytes = new Uint8Array(32)
395
+ globalThis.crypto.getRandomValues(bytes)
396
+ let hex = ''
397
+ for (let i = 0; i < bytes.length; i++) {
398
+ hex += bytes[i]?.toString(16).padStart(2, '0')
399
+ }
400
+ return hex
401
+ }
@@ -0,0 +1,50 @@
1
+ import { getContext, setContext } from 'svelte'
2
+ import { type AuthSession, createAuthSession } from '../bindings/create-auth-session'
3
+ import type { AuthClient, AuthState } from '../client/auth-client'
4
+
5
+ // Registered key: AuthProvider.svelte is compiled from src/ while useAuth comes from dist/,
6
+ // so two copies of this module exist in an app and must share the key.
7
+ const authContextKey = Symbol.for('@korajs/auth:svelte-auth-context')
8
+
9
+ export interface AuthContextValue {
10
+ client: AuthClient
11
+ session: AuthSession
12
+ get state(): AuthState
13
+ get isLoading(): boolean
14
+ }
15
+
16
+ /**
17
+ * Initialize auth context inside a Svelte component (root layout).
18
+ * Must be called during component initialization before children render.
19
+ */
20
+ export function initAuthProvider(client: AuthClient): AuthContextValue {
21
+ const session = createAuthSession(client)
22
+
23
+ const value: AuthContextValue = {
24
+ client,
25
+ session,
26
+ get state() {
27
+ return session.getSnapshot().state
28
+ },
29
+ get isLoading() {
30
+ return session.getSnapshot().isLoading
31
+ },
32
+ }
33
+
34
+ setContext(authContextKey, value)
35
+ return value
36
+ }
37
+
38
+ export function getAuthContext(): AuthContextValue {
39
+ const context = getContext<AuthContextValue | undefined>(authContextKey)
40
+ if (!context) {
41
+ throw new Error(
42
+ 'Auth context missing. Call initAuthProvider(client) in your root layout component.',
43
+ )
44
+ }
45
+ return context
46
+ }
47
+
48
+ export function destroyAuthProvider(context: AuthContextValue): void {
49
+ context.session.destroy()
50
+ }
@@ -0,0 +1,32 @@
1
+ import { getContext, setContext } from 'svelte'
2
+ import { type OrgSession, createOrgSession } from '../bindings/create-org-session'
3
+ import type { OrgClient } from '../client/org-client'
4
+
5
+ // Registered key: OrgProvider.svelte (src/) and the org hooks (dist/) must share it.
6
+ const orgContextKey = Symbol.for('@korajs/auth:svelte-org-context')
7
+
8
+ export interface OrgContextValue {
9
+ client: OrgClient
10
+ session: OrgSession
11
+ }
12
+
13
+ export function initOrgProvider(client: OrgClient): OrgContextValue {
14
+ const session = createOrgSession(client)
15
+ const value: OrgContextValue = { client, session }
16
+ setContext(orgContextKey, value)
17
+ return value
18
+ }
19
+
20
+ export function getOrgContext(): OrgContextValue {
21
+ const context = getContext<OrgContextValue | undefined>(orgContextKey)
22
+ if (!context) {
23
+ throw new Error(
24
+ 'Org context missing. Wrap your app with <OrgProvider client={orgClient}> from @korajs/auth/svelte.',
25
+ )
26
+ }
27
+ return context
28
+ }
29
+
30
+ export function destroyOrgProvider(context: OrgContextValue): void {
31
+ context.session.destroy()
32
+ }
@@ -0,0 +1,201 @@
1
+ import { type Readable, derived, get, readable, writable } from 'svelte/store'
2
+ import {
3
+ checkOrgPermission,
4
+ createOrgMembersActions,
5
+ loadOrgMembers,
6
+ } from '../bindings/create-org-session'
7
+ import type { ClientInvitation, ClientMembership, ClientOrganization } from '../client/org-client'
8
+ import { getOrgContext } from './org-context'
9
+
10
+ export interface UseOrgResult {
11
+ subscribe: Readable<{
12
+ org: ClientOrganization | null
13
+ role: string | null
14
+ orgId: string | null
15
+ error: string | null
16
+ }>['subscribe']
17
+ get org(): ClientOrganization | null
18
+ get role(): string | null
19
+ get orgId(): string | null
20
+ switchOrg: (orgId: string) => Promise<void>
21
+ createOrg: (params: { name: string; slug?: string }) => Promise<ClientOrganization>
22
+ leaveOrg: () => Promise<void>
23
+ clearOrg: () => void
24
+ listOrgs: () => Promise<ClientOrganization[]>
25
+ }
26
+
27
+ export interface UseOrgMembersResult {
28
+ subscribe: Readable<{
29
+ members: ClientMembership[]
30
+ isLoading: boolean
31
+ error: string | null
32
+ }>['subscribe']
33
+ refresh: () => Promise<void>
34
+ invite: (email: string, role: string) => Promise<ClientInvitation>
35
+ removeMember: (userId: string) => Promise<void>
36
+ updateRole: (userId: string, role: string) => Promise<void>
37
+ }
38
+
39
+ function createOrgSnapshotStore(): Readable<{
40
+ org: ClientOrganization | null
41
+ role: string | null
42
+ orgId: string | null
43
+ }> {
44
+ const { session } = getOrgContext()
45
+
46
+ return readable(session.getSnapshot(), (set) => {
47
+ set(session.getSnapshot())
48
+ return session.subscribe(() => {
49
+ set(session.getSnapshot())
50
+ })
51
+ })
52
+ }
53
+
54
+ export function useOrg(): UseOrgResult {
55
+ const { client, session } = getOrgContext()
56
+ const snapshotStore = createOrgSnapshotStore()
57
+ const error = writable<string | null>(null)
58
+
59
+ const store = derived([snapshotStore, error], ([snapshot, errorValue]) => ({
60
+ ...snapshot,
61
+ error: errorValue,
62
+ }))
63
+
64
+ const switchOrg = async (orgId: string): Promise<void> => {
65
+ error.set(null)
66
+ try {
67
+ await client.switchOrg(orgId)
68
+ } catch (err) {
69
+ error.set(err instanceof Error ? err.message : String(err))
70
+ }
71
+ }
72
+
73
+ const createOrg = async (params: {
74
+ name: string
75
+ slug?: string
76
+ }): Promise<ClientOrganization> => {
77
+ error.set(null)
78
+ try {
79
+ return await client.createOrg(params)
80
+ } catch (err) {
81
+ const message = err instanceof Error ? err.message : String(err)
82
+ error.set(message)
83
+ throw err
84
+ }
85
+ }
86
+
87
+ const leaveOrg = async (): Promise<void> => {
88
+ const orgId = get(snapshotStore).orgId
89
+ if (!orgId) return
90
+ error.set(null)
91
+ try {
92
+ await client.leaveOrg(orgId)
93
+ } catch (err) {
94
+ error.set(err instanceof Error ? err.message : String(err))
95
+ }
96
+ }
97
+
98
+ const clearOrg = (): void => {
99
+ client.clearActiveOrg()
100
+ }
101
+
102
+ const listOrgs = async (): Promise<ClientOrganization[]> => {
103
+ try {
104
+ return await client.listOrgs()
105
+ } catch (err) {
106
+ error.set(err instanceof Error ? err.message : String(err))
107
+ return []
108
+ }
109
+ }
110
+
111
+ return {
112
+ subscribe: store.subscribe,
113
+ get org() {
114
+ return get(snapshotStore).org
115
+ },
116
+ get role() {
117
+ return get(snapshotStore).role
118
+ },
119
+ get orgId() {
120
+ return get(snapshotStore).orgId
121
+ },
122
+ switchOrg,
123
+ createOrg,
124
+ leaveOrg,
125
+ clearOrg,
126
+ listOrgs,
127
+ }
128
+ }
129
+
130
+ export function useOrgMembers(orgId: string): UseOrgMembersResult {
131
+ const { client } = getOrgContext()
132
+ const members = writable<ClientMembership[]>([])
133
+ const isLoading = writable(true)
134
+ const error = writable<string | null>(null)
135
+
136
+ const refresh = async (targetOrgId: string): Promise<void> => {
137
+ isLoading.set(true)
138
+ error.set(null)
139
+ try {
140
+ members.set(await loadOrgMembers(client, targetOrgId))
141
+ } catch (err) {
142
+ error.set(err instanceof Error ? err.message : String(err))
143
+ } finally {
144
+ isLoading.set(false)
145
+ }
146
+ }
147
+
148
+ $effect(() => {
149
+ void refresh(orgId)
150
+ })
151
+
152
+ const actions = createOrgMembersActions(client, orgId, (message) => {
153
+ error.set(message)
154
+ })
155
+
156
+ const invite = async (email: string, role: string): Promise<ClientInvitation> => {
157
+ const result = await actions.invite(email, role)
158
+ await refresh(orgId)
159
+ return result
160
+ }
161
+
162
+ const removeMember = async (userId: string): Promise<void> => {
163
+ await actions.removeMember(userId)
164
+ await refresh(orgId)
165
+ }
166
+
167
+ const updateRole = async (userId: string, role: string): Promise<void> => {
168
+ await actions.updateRole(userId, role)
169
+ await refresh(orgId)
170
+ }
171
+
172
+ const store = derived([members, isLoading, error], ([memberList, loading, errorValue]) => ({
173
+ members: memberList,
174
+ isLoading: loading,
175
+ error: errorValue,
176
+ }))
177
+
178
+ return {
179
+ subscribe: store.subscribe,
180
+ refresh: () => refresh(orgId),
181
+ invite,
182
+ removeMember,
183
+ updateRole,
184
+ }
185
+ }
186
+
187
+ export function createPermissionStore(requiredRole: string): Readable<boolean> {
188
+ const { session } = getOrgContext()
189
+
190
+ return readable(session.checkPermission(requiredRole), (set) => {
191
+ set(session.checkPermission(requiredRole))
192
+ return session.subscribe(() => {
193
+ set(session.checkPermission(requiredRole))
194
+ })
195
+ })
196
+ }
197
+
198
+ /** Alias for {@link createPermissionStore}. */
199
+ export const usePermission = createPermissionStore
200
+
201
+ export { checkOrgPermission }