@korajs/auth 1.0.0-beta.12 → 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,210 @@
1
+ import { KoraError } from '@korajs/core'
2
+
3
+ // ============================================================================
4
+ // Permissions
5
+ // ============================================================================
6
+
7
+ /**
8
+ * A permission is a `resource:action` string.
9
+ *
10
+ * Resources are typically collection names or system resources.
11
+ * Actions are the operations allowed on that resource.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const permission: Permission = 'todos:write'
16
+ * const adminPerm: Permission = 'org:manage-members'
17
+ * const wildcard: Permission = 'todos:*' // all actions on todos
18
+ * const superAdmin: Permission = '*:*' // all actions on all resources
19
+ * ```
20
+ */
21
+ export type Permission = `${string}:${string}`
22
+
23
+ /**
24
+ * Parse a permission string into its resource and action parts.
25
+ */
26
+ export function parsePermission(permission: Permission): { resource: string; action: string } {
27
+ const colonIndex = permission.indexOf(':')
28
+ if (colonIndex === -1) {
29
+ throw new InvalidPermissionError(permission)
30
+ }
31
+ const resource = permission.slice(0, colonIndex)
32
+ const action = permission.slice(colonIndex + 1)
33
+ if (resource.length === 0 || action.length === 0) {
34
+ throw new InvalidPermissionError(permission)
35
+ }
36
+ return { resource, action }
37
+ }
38
+
39
+ /**
40
+ * Check if a granted permission covers a required permission.
41
+ * Supports wildcards: `todos:*` covers `todos:read`, `*:*` covers everything.
42
+ */
43
+ export function permissionCovers(granted: Permission, required: Permission): boolean {
44
+ const g = parsePermission(granted)
45
+ const r = parsePermission(required)
46
+
47
+ const resourceMatch = g.resource === '*' || g.resource === r.resource
48
+ const actionMatch = g.action === '*' || g.action === r.action
49
+ return resourceMatch && actionMatch
50
+ }
51
+
52
+ // ============================================================================
53
+ // Role Definitions
54
+ // ============================================================================
55
+
56
+ /**
57
+ * A role definition describes a named set of permissions with optional inheritance.
58
+ *
59
+ * Roles form a hierarchy via `inherits`. When evaluating permissions,
60
+ * all inherited roles' permissions are included transitively.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const roles: RoleDefinition[] = [
65
+ * { name: 'viewer', permissions: ['todos:read', 'projects:read'] },
66
+ * { name: 'member', permissions: ['todos:write', 'projects:write'], inherits: ['viewer'] },
67
+ * { name: 'admin', permissions: ['org:manage-members'], inherits: ['member'] },
68
+ * ]
69
+ * ```
70
+ */
71
+ export interface RoleDefinition {
72
+ /** Unique name for this role */
73
+ name: string
74
+ /** Permissions directly granted to this role */
75
+ permissions: Permission[]
76
+ /** Roles this role inherits from (all their permissions are included) */
77
+ inherits?: string[]
78
+ }
79
+
80
+ // ============================================================================
81
+ // Built-in Roles
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Default built-in roles for organizations.
86
+ *
87
+ * These can be overridden or extended using `defineRoles()`.
88
+ */
89
+ export const BUILT_IN_ROLES: readonly RoleDefinition[] = [
90
+ {
91
+ name: 'viewer',
92
+ permissions: ['*:read'],
93
+ },
94
+ {
95
+ name: 'billing',
96
+ permissions: ['org:billing'],
97
+ },
98
+ {
99
+ name: 'member',
100
+ permissions: ['*:write', '*:delete'],
101
+ inherits: ['viewer'],
102
+ },
103
+ {
104
+ name: 'admin',
105
+ permissions: ['org:manage-members', 'org:manage-settings', 'org:manage-invitations'],
106
+ inherits: ['member'],
107
+ },
108
+ {
109
+ name: 'owner',
110
+ permissions: ['*:*'],
111
+ },
112
+ ] as const
113
+
114
+ // ============================================================================
115
+ // RBAC Configuration
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Configuration for the RBAC engine.
120
+ */
121
+ export interface RbacConfig {
122
+ /** Role definitions. Defaults to BUILT_IN_ROLES if not provided. */
123
+ roles?: RoleDefinition[]
124
+ }
125
+
126
+ // ============================================================================
127
+ // Scope Types
128
+ // ============================================================================
129
+
130
+ /**
131
+ * A sync scope filter for a single collection.
132
+ * The keys are field names and values are the required values.
133
+ * A special `__readonly` key can be set to restrict to read-only access.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * // Only sync todos belonging to the user within the org
138
+ * const scope: ScopeFilter = { orgId: 'org-123', userId: 'user-456' }
139
+ *
140
+ * // Read-only scope for viewers
141
+ * const readOnlyScope: ScopeFilter = { orgId: 'org-123', __readonly: true }
142
+ * ```
143
+ */
144
+ export interface ScopeFilter {
145
+ [field: string]: unknown
146
+ }
147
+
148
+ /**
149
+ * A complete set of sync scopes, keyed by collection name.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const scopes: SyncScopes = {
154
+ * todos: { orgId: 'org-123' },
155
+ * projects: { orgId: 'org-123' },
156
+ * }
157
+ * ```
158
+ */
159
+ export type SyncScopes = Record<string, ScopeFilter>
160
+
161
+ /**
162
+ * Custom scope resolver for a specific collection.
163
+ * Given the user context, returns the scope filter for that collection.
164
+ */
165
+ export type CollectionScopeResolver = (ctx: ScopeContext) => ScopeFilter | null
166
+
167
+ /**
168
+ * Context passed to scope resolvers.
169
+ */
170
+ export interface ScopeContext {
171
+ userId: string
172
+ orgId: string
173
+ role: string
174
+ permissions: Permission[]
175
+ }
176
+
177
+ // ============================================================================
178
+ // Errors
179
+ // ============================================================================
180
+
181
+ export class RbacError extends KoraError {
182
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
183
+ super(message, code, context)
184
+ this.name = 'RbacError'
185
+ }
186
+ }
187
+
188
+ export class InvalidPermissionError extends RbacError {
189
+ constructor(permission: string) {
190
+ super(
191
+ `Invalid permission format: "${permission}". Expected "resource:action".`,
192
+ 'INVALID_PERMISSION',
193
+ { permission },
194
+ )
195
+ }
196
+ }
197
+
198
+ export class RoleNotFoundError extends RbacError {
199
+ constructor(role: string) {
200
+ super(`Role "${role}" is not defined.`, 'ROLE_NOT_FOUND', { role })
201
+ }
202
+ }
203
+
204
+ export class CircularInheritanceError extends RbacError {
205
+ constructor(chain: string[]) {
206
+ super(`Circular role inheritance detected: ${chain.join(' → ')}.`, 'CIRCULAR_INHERITANCE', {
207
+ chain,
208
+ })
209
+ }
210
+ }
@@ -0,0 +1,140 @@
1
+ import type { OrgStore } from '../org/org-store'
2
+ import type { OrgRole } from '../org/org-types'
3
+ import type { RbacEngine } from './rbac-engine'
4
+ import type {
5
+ CollectionScopeResolver,
6
+ Permission,
7
+ ScopeContext,
8
+ ScopeFilter,
9
+ SyncScopes,
10
+ } from './rbac-types'
11
+ import { permissionCovers } from './rbac-types'
12
+
13
+ // ============================================================================
14
+ // OrgScopeResolver
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Resolves sync scopes for org-aware data filtering.
19
+ *
20
+ * Given a user, an organization, and a set of collections, this resolver
21
+ * determines what data the user should receive during sync:
22
+ *
23
+ * - **Owner/Admin**: All data in the org (no field-level filtering)
24
+ * - **Member**: All org data with read/write access
25
+ * - **Viewer**: All org data with read-only flag
26
+ * - **Billing**: No data access (empty scopes)
27
+ *
28
+ * Developers can register per-collection scope resolvers for fine-grained control.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const resolver = new OrgScopeResolver(orgStore, rbacEngine)
33
+ *
34
+ * // Custom scope: members only see their own todos
35
+ * resolver.registerCollectionScope('todos', (ctx) => {
36
+ * if (ctx.role === 'member') {
37
+ * return { orgId: ctx.orgId, userId: ctx.userId }
38
+ * }
39
+ * return { orgId: ctx.orgId } // admins/owners see all
40
+ * })
41
+ *
42
+ * const scopes = await resolver.resolve('user-1', 'org-1', ['todos', 'projects'])
43
+ * // { todos: { orgId: 'org-1', userId: 'user-1' }, projects: { orgId: 'org-1' } }
44
+ * ```
45
+ */
46
+ export class OrgScopeResolver {
47
+ private readonly orgStore: OrgStore
48
+ private readonly rbac: RbacEngine
49
+ private readonly collectionScopes = new Map<string, CollectionScopeResolver>()
50
+
51
+ constructor(orgStore: OrgStore, rbac: RbacEngine) {
52
+ this.orgStore = orgStore
53
+ this.rbac = rbac
54
+ }
55
+
56
+ /**
57
+ * Register a custom scope resolver for a collection.
58
+ * This overrides the default orgId-based filtering for that collection.
59
+ */
60
+ registerCollectionScope(collection: string, resolver: CollectionScopeResolver): void {
61
+ this.collectionScopes.set(collection, resolver)
62
+ }
63
+
64
+ /**
65
+ * Resolve sync scopes for all specified collections.
66
+ *
67
+ * Returns null if the user is not a member of the organization.
68
+ * Returns an empty object if the user has no data access (e.g., billing role).
69
+ */
70
+ async resolve(userId: string, orgId: string, collections: string[]): Promise<SyncScopes | null> {
71
+ const membership = await this.orgStore.getMembership(orgId, userId)
72
+ if (!membership) return null
73
+
74
+ const permissions = this.rbac.getRolePermissions(membership.role)
75
+ const ctx: ScopeContext = {
76
+ userId,
77
+ orgId,
78
+ role: membership.role,
79
+ permissions,
80
+ }
81
+
82
+ const scopes: SyncScopes = {}
83
+
84
+ for (const collection of collections) {
85
+ const scope = this.resolveCollectionScope(ctx, collection)
86
+ if (scope) {
87
+ scopes[collection] = scope
88
+ }
89
+ }
90
+
91
+ return scopes
92
+ }
93
+
94
+ /**
95
+ * Check if a user can write to a specific collection in an org.
96
+ */
97
+ async canWrite(userId: string, orgId: string, collection: string): Promise<boolean> {
98
+ return this.rbac.hasPermission(userId, orgId, `${collection}:write` as Permission)
99
+ }
100
+
101
+ /**
102
+ * Check if a user can read from a specific collection in an org.
103
+ */
104
+ async canRead(userId: string, orgId: string, collection: string): Promise<boolean> {
105
+ return this.rbac.hasPermission(userId, orgId, `${collection}:read` as Permission)
106
+ }
107
+
108
+ // --- Private ---
109
+
110
+ private resolveCollectionScope(ctx: ScopeContext, collection: string): ScopeFilter | null {
111
+ // Check if user has read permission for this collection
112
+ const canRead = ctx.permissions.some(
113
+ (p) =>
114
+ permissionCovers(p, `${collection}:read` as Permission) ||
115
+ permissionCovers(p, '*:read' as Permission),
116
+ )
117
+ if (!canRead) return null
118
+
119
+ // Check custom resolver
120
+ const customResolver = this.collectionScopes.get(collection)
121
+ if (customResolver) {
122
+ return customResolver(ctx)
123
+ }
124
+
125
+ // Default scope: filter by orgId
126
+ const scope: ScopeFilter = { orgId: ctx.orgId }
127
+
128
+ // Check if write access is available
129
+ const canWrite = ctx.permissions.some(
130
+ (p) =>
131
+ permissionCovers(p, `${collection}:write` as Permission) ||
132
+ permissionCovers(p, '*:write' as Permission),
133
+ )
134
+ if (!canWrite) {
135
+ scope.__readonly = true
136
+ }
137
+
138
+ return scope
139
+ }
140
+ }
@@ -0,0 +1,97 @@
1
+ import { createElement, useEffect, useMemo, useSyncExternalStore } from 'react'
2
+ import type { ReactElement, ReactNode } from 'react'
3
+ import { createAuthSession } from '../bindings/create-auth-session'
4
+ import type { AuthSession } from '../bindings/create-auth-session'
5
+ import type { AuthClient } from '../client/auth-client'
6
+ import { AuthContext } from './auth-context'
7
+
8
+ interface AuthProviderProps {
9
+ client: AuthClient
10
+ children: ReactNode
11
+ fallback?: ReactNode
12
+ }
13
+
14
+ type SessionEntry = {
15
+ session: AuthSession
16
+ refs: number
17
+ destroyTimer: ReturnType<typeof setTimeout> | null
18
+ }
19
+
20
+ const sessionEntries = new WeakMap<AuthClient, SessionEntry>()
21
+
22
+ function getSessionEntry(client: AuthClient): SessionEntry {
23
+ const existing = sessionEntries.get(client)
24
+ if (existing) return existing
25
+
26
+ const entry: SessionEntry = {
27
+ session: createAuthSession(client),
28
+ refs: 0,
29
+ destroyTimer: null,
30
+ }
31
+ sessionEntries.set(client, entry)
32
+ return entry
33
+ }
34
+
35
+ function retainSession(entry: SessionEntry): () => void {
36
+ if (entry.destroyTimer) {
37
+ clearTimeout(entry.destroyTimer)
38
+ entry.destroyTimer = null
39
+ }
40
+ entry.refs++
41
+
42
+ return () => {
43
+ entry.refs = Math.max(0, entry.refs - 1)
44
+ if (entry.refs > 0 || entry.destroyTimer) return
45
+
46
+ // React StrictMode intentionally runs effect cleanup and setup back-to-back
47
+ // on mount. Deferring disposal lets the second setup retain the same session,
48
+ // while a real unmount still releases the auth subscription shortly after.
49
+ entry.destroyTimer = setTimeout(() => {
50
+ entry.destroyTimer = null
51
+ if (entry.refs > 0) return
52
+ entry.session.destroy()
53
+ sessionEntries.delete(entry.session.client)
54
+ }, 0)
55
+ }
56
+ }
57
+
58
+ function AuthProvider({ client, children, fallback }: AuthProviderProps): ReactElement {
59
+ const entry = useMemo(() => getSessionEntry(client), [client])
60
+ const session = entry.session
61
+
62
+ useEffect(() => retainSession(entry), [entry])
63
+
64
+ const snapshot = useSyncExternalStore(
65
+ (onStoreChange) => session.subscribe(onStoreChange),
66
+ () => session.getSnapshot(),
67
+ () => session.getSnapshot(),
68
+ )
69
+
70
+ if (snapshot.initError) {
71
+ return createElement(
72
+ 'div',
73
+ {
74
+ style: { color: 'red', padding: '1rem', fontFamily: 'monospace' },
75
+ role: 'alert',
76
+ },
77
+ createElement('strong', null, 'Kora Auth initialization error: '),
78
+ snapshot.initError.message,
79
+ )
80
+ }
81
+
82
+ if (snapshot.isLoading && fallback !== undefined) {
83
+ return fallback as ReactElement
84
+ }
85
+
86
+ const contextValue = {
87
+ client,
88
+ session,
89
+ state: snapshot.state,
90
+ isLoading: snapshot.isLoading,
91
+ }
92
+
93
+ return createElement(AuthContext.Provider, { value: contextValue }, children)
94
+ }
95
+
96
+ export { AuthProvider }
97
+ export type { AuthProviderProps }
@@ -0,0 +1,41 @@
1
+ import { type ReactNode, useEffect, useMemo, useRef } from 'react'
2
+ import { type OrgSession, createOrgSession } from '../bindings/create-org-session'
3
+ import type { OrgClient } from '../client/org-client'
4
+ import { OrgContext, type OrgContextValue } from './org-hooks'
5
+
6
+ export interface OrgProviderProps {
7
+ client: OrgClient
8
+ children?: ReactNode
9
+ }
10
+
11
+ /**
12
+ * Provides organization context for {@link useOrg}, {@link useOrgMembers}, and {@link usePermission}.
13
+ */
14
+ export function OrgProvider({ client, children }: OrgProviderProps) {
15
+ const sessionRef = useRef<OrgSession | null>(null)
16
+
17
+ if (sessionRef.current === null || sessionRef.current.client !== client) {
18
+ sessionRef.current?.destroy()
19
+ sessionRef.current = createOrgSession(client)
20
+ }
21
+
22
+ useEffect(() => {
23
+ return () => {
24
+ sessionRef.current?.destroy()
25
+ sessionRef.current = null
26
+ }
27
+ }, [])
28
+
29
+ const session = sessionRef.current
30
+ const value = useMemo<OrgContextValue>(
31
+ () => ({
32
+ client,
33
+ session,
34
+ }),
35
+ [client, session],
36
+ )
37
+
38
+ return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>
39
+ }
40
+
41
+ export type { OrgContextValue }
@@ -0,0 +1,26 @@
1
+ import { createContext } from 'react'
2
+ import type { AuthSession } from '../bindings/create-auth-session'
3
+ import type { AuthClient } from '../client/auth-client'
4
+
5
+ /**
6
+ * Possible authentication states for the client.
7
+ */
8
+ type AuthState = 'loading' | 'authenticated' | 'unauthenticated'
9
+
10
+ /**
11
+ * Shape of the value provided by the AuthContext.
12
+ */
13
+ interface AuthContextValue {
14
+ client: AuthClient
15
+ session: AuthSession
16
+ state: AuthState
17
+ isLoading: boolean
18
+ }
19
+
20
+ /**
21
+ * React context for Kora authentication.
22
+ */
23
+ const AuthContext = createContext<AuthContextValue | null>(null)
24
+
25
+ export { AuthContext }
26
+ export type { AuthContextValue, AuthState }
@@ -0,0 +1,110 @@
1
+ import { useContext, useSyncExternalStore } from 'react'
2
+ import type {
3
+ AuthState,
4
+ AuthUser,
5
+ LinkedOAuthAccount,
6
+ OAuthAuthorizationOptions,
7
+ OAuthAuthorizationResult,
8
+ OAuthCallbackParams,
9
+ } from '../client/auth-client'
10
+ import { AuthContext } from './auth-context'
11
+
12
+ function useAuthContext() {
13
+ const ctx = useContext(AuthContext)
14
+ if (ctx === null) {
15
+ throw new Error(
16
+ 'useAuth / useCurrentUser / useAuthStatus must be used within an <AuthProvider>. ' +
17
+ 'Wrap your component tree with <AuthProvider client={authClient}>.',
18
+ )
19
+ }
20
+ return ctx
21
+ }
22
+
23
+ interface UseAuthResult {
24
+ user: AuthUser | null
25
+ isAuthenticated: boolean
26
+ isLoading: boolean
27
+ signUp: (params: {
28
+ email: string
29
+ password: string
30
+ name?: string
31
+ deviceId?: string
32
+ devicePublicKey?: string
33
+ }) => Promise<void>
34
+ signIn: (params: {
35
+ email: string
36
+ password: string
37
+ deviceId?: string
38
+ devicePublicKey?: string
39
+ }) => Promise<void>
40
+ signInWithOAuth: (
41
+ provider: string,
42
+ options?: OAuthAuthorizationOptions,
43
+ ) => Promise<OAuthAuthorizationResult>
44
+ completeOAuthSignIn: (provider: string, params: OAuthCallbackParams) => Promise<void>
45
+ getOAuthAuthorizationUrl: (
46
+ provider: string,
47
+ options?: OAuthAuthorizationOptions,
48
+ ) => Promise<OAuthAuthorizationResult>
49
+ linkOAuth: (provider: string, params: OAuthCallbackParams) => Promise<LinkedOAuthAccount | null>
50
+ listLinkedAccounts: () => Promise<LinkedOAuthAccount[]>
51
+ unlinkOAuth: (provider: string) => Promise<void>
52
+ signOut: () => Promise<void>
53
+ error: string | null
54
+ initError: Error | null
55
+ }
56
+
57
+ interface AuthStatus {
58
+ state: AuthState
59
+ isAuthenticated: boolean
60
+ isLoading: boolean
61
+ }
62
+
63
+ function useAuthSessionSnapshot() {
64
+ const { session } = useAuthContext()
65
+
66
+ return useSyncExternalStore(
67
+ (onStoreChange) => session.subscribe(onStoreChange),
68
+ () => session.getSnapshot(),
69
+ () => session.getSnapshot(),
70
+ )
71
+ }
72
+
73
+ function useAuth(): UseAuthResult {
74
+ const { session } = useAuthContext()
75
+ const snapshot = useAuthSessionSnapshot()
76
+
77
+ return {
78
+ user: snapshot.user,
79
+ isAuthenticated: snapshot.isAuthenticated,
80
+ isLoading: snapshot.isLoading,
81
+ error: snapshot.error,
82
+ initError: snapshot.initError,
83
+ signUp: (params) => session.signUp(params),
84
+ signIn: (params) => session.signIn(params),
85
+ signInWithOAuth: (provider, options) => session.signInWithOAuth(provider, options),
86
+ completeOAuthSignIn: (provider, params) => session.completeOAuthSignIn(provider, params),
87
+ getOAuthAuthorizationUrl: (provider, options) =>
88
+ session.getOAuthAuthorizationUrl(provider, options),
89
+ linkOAuth: (provider, params) => session.linkOAuth(provider, params),
90
+ listLinkedAccounts: () => session.listLinkedAccounts(),
91
+ unlinkOAuth: (provider) => session.unlinkOAuth(provider),
92
+ signOut: () => session.signOut(),
93
+ }
94
+ }
95
+
96
+ function useCurrentUser(): AuthUser | null {
97
+ return useAuthSessionSnapshot().user
98
+ }
99
+
100
+ function useAuthStatus(): AuthStatus {
101
+ const snapshot = useAuthSessionSnapshot()
102
+ return {
103
+ state: snapshot.state,
104
+ isAuthenticated: snapshot.isAuthenticated,
105
+ isLoading: snapshot.isLoading,
106
+ }
107
+ }
108
+
109
+ export { useAuth, useCurrentUser, useAuthStatus }
110
+ export type { UseAuthResult, AuthStatus }