@geenius/adapters 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/workflows/ci.yml +23 -0
  8. package/.github/workflows/release.yml +29 -0
  9. package/.nvmrc +1 -0
  10. package/.project/ACCOUNT.yaml +4 -0
  11. package/.project/IDEAS.yaml +7 -0
  12. package/.project/PROJECT.yaml +11 -0
  13. package/.project/ROADMAP.yaml +15 -0
  14. package/CHANGELOG.md +11 -0
  15. package/CODE_OF_CONDUCT.md +16 -0
  16. package/CONTRIBUTING.md +26 -0
  17. package/LICENSE +21 -0
  18. package/README.md +202 -0
  19. package/SECURITY.md +15 -0
  20. package/SUPPORT.md +8 -0
  21. package/package.json +51 -0
  22. package/packages/convex/README.md +64 -0
  23. package/packages/convex/package.json +42 -0
  24. package/packages/convex/src/adapter.ts +39 -0
  25. package/packages/convex/src/index.ts +19 -0
  26. package/packages/convex/src/mutations.ts +142 -0
  27. package/packages/convex/src/queries.ts +106 -0
  28. package/packages/convex/src/schema.ts +54 -0
  29. package/packages/convex/src/types.ts +20 -0
  30. package/packages/convex/tsconfig.json +11 -0
  31. package/packages/convex/tsup.config.ts +10 -0
  32. package/packages/react/README.md +1 -0
  33. package/packages/react/package.json +45 -0
  34. package/packages/react/src/components/AdapterCard.tsx +49 -0
  35. package/packages/react/src/components/AdapterConfigForm.tsx +118 -0
  36. package/packages/react/src/components/AdapterList.tsx +84 -0
  37. package/packages/react/src/components/AdapterStatusBadge.tsx +30 -0
  38. package/packages/react/src/components/index.ts +4 -0
  39. package/packages/react/src/hooks/index.ts +75 -0
  40. package/packages/react/src/index.tsx +44 -0
  41. package/packages/react/src/pages/AdapterDetailPage.tsx +133 -0
  42. package/packages/react/src/pages/AdaptersPage.tsx +111 -0
  43. package/packages/react/src/pages/index.ts +2 -0
  44. package/packages/react/src/provider/AdapterProvider.tsx +115 -0
  45. package/packages/react/src/provider/index.ts +2 -0
  46. package/packages/react/tsconfig.json +18 -0
  47. package/packages/react/tsup.config.ts +10 -0
  48. package/packages/react-css/README.md +1 -0
  49. package/packages/react-css/package.json +44 -0
  50. package/packages/react-css/src/adapters.css +1576 -0
  51. package/packages/react-css/src/components/AdapterCard.tsx +34 -0
  52. package/packages/react-css/src/components/AdapterConfigForm.tsx +63 -0
  53. package/packages/react-css/src/components/AdapterList.tsx +40 -0
  54. package/packages/react-css/src/components/AdapterStatusBadge.tsx +21 -0
  55. package/packages/react-css/src/components/index.ts +4 -0
  56. package/packages/react-css/src/hooks/index.ts +75 -0
  57. package/packages/react-css/src/index.tsx +25 -0
  58. package/packages/react-css/src/pages/AdapterDetailPage.tsx +133 -0
  59. package/packages/react-css/src/pages/AdaptersPage.tsx +111 -0
  60. package/packages/react-css/src/pages/index.ts +2 -0
  61. package/packages/react-css/src/provider/AdapterProvider.tsx +115 -0
  62. package/packages/react-css/src/provider/index.ts +2 -0
  63. package/packages/react-css/src/styles.css +494 -0
  64. package/packages/react-css/tsconfig.json +19 -0
  65. package/packages/react-css/tsup.config.ts +2 -0
  66. package/packages/shared/README.md +1 -0
  67. package/packages/shared/package.json +39 -0
  68. package/packages/shared/src/__tests__/adapters.test.ts +545 -0
  69. package/packages/shared/src/admin/index.ts +2 -0
  70. package/packages/shared/src/admin/interface.ts +34 -0
  71. package/packages/shared/src/admin/localStorage.ts +109 -0
  72. package/packages/shared/src/ai/anthropic.ts +123 -0
  73. package/packages/shared/src/ai/cloudflare-gateway.ts +130 -0
  74. package/packages/shared/src/ai/gemini.ts +181 -0
  75. package/packages/shared/src/ai/index.ts +14 -0
  76. package/packages/shared/src/ai/interface.ts +11 -0
  77. package/packages/shared/src/ai/localStorage.ts +78 -0
  78. package/packages/shared/src/ai/ollama.ts +143 -0
  79. package/packages/shared/src/ai/openai.ts +120 -0
  80. package/packages/shared/src/ai/vercel-ai.ts +101 -0
  81. package/packages/shared/src/auth/better-auth.ts +118 -0
  82. package/packages/shared/src/auth/clerk.ts +151 -0
  83. package/packages/shared/src/auth/convex-auth.ts +125 -0
  84. package/packages/shared/src/auth/index.ts +10 -0
  85. package/packages/shared/src/auth/interface.ts +17 -0
  86. package/packages/shared/src/auth/localStorage.ts +125 -0
  87. package/packages/shared/src/auth/supabase-auth.ts +136 -0
  88. package/packages/shared/src/config.ts +57 -0
  89. package/packages/shared/src/constants.ts +122 -0
  90. package/packages/shared/src/db/convex.ts +146 -0
  91. package/packages/shared/src/db/index.ts +10 -0
  92. package/packages/shared/src/db/interface.ts +13 -0
  93. package/packages/shared/src/db/localStorage.ts +91 -0
  94. package/packages/shared/src/db/mongodb.ts +125 -0
  95. package/packages/shared/src/db/neon.ts +171 -0
  96. package/packages/shared/src/db/supabase.ts +158 -0
  97. package/packages/shared/src/index.ts +117 -0
  98. package/packages/shared/src/payments/index.ts +4 -0
  99. package/packages/shared/src/payments/interface.ts +11 -0
  100. package/packages/shared/src/payments/localStorage.ts +81 -0
  101. package/packages/shared/src/payments/stripe.ts +177 -0
  102. package/packages/shared/src/storage/convex.ts +113 -0
  103. package/packages/shared/src/storage/index.ts +14 -0
  104. package/packages/shared/src/storage/interface.ts +11 -0
  105. package/packages/shared/src/storage/localStorage.ts +95 -0
  106. package/packages/shared/src/storage/minio.ts +47 -0
  107. package/packages/shared/src/storage/r2.ts +123 -0
  108. package/packages/shared/src/storage/s3.ts +128 -0
  109. package/packages/shared/src/storage/supabase-storage.ts +116 -0
  110. package/packages/shared/src/storage/uploadthing.ts +126 -0
  111. package/packages/shared/src/styles/adapters.css +494 -0
  112. package/packages/shared/src/tier-gate.ts +119 -0
  113. package/packages/shared/src/types.ts +162 -0
  114. package/packages/shared/tsconfig.json +18 -0
  115. package/packages/shared/tsup.config.ts +9 -0
  116. package/packages/shared/vitest.config.ts +14 -0
  117. package/packages/solidjs/README.md +1 -0
  118. package/packages/solidjs/package.json +44 -0
  119. package/packages/solidjs/src/components/AdapterCard.tsx +24 -0
  120. package/packages/solidjs/src/components/AdapterConfigForm.tsx +54 -0
  121. package/packages/solidjs/src/components/AdapterList.tsx +28 -0
  122. package/packages/solidjs/src/components/AdapterStatusBadge.tsx +20 -0
  123. package/packages/solidjs/src/components/index.ts +4 -0
  124. package/packages/solidjs/src/index.tsx +17 -0
  125. package/packages/solidjs/src/pages/AdapterDetailPage.tsx +38 -0
  126. package/packages/solidjs/src/pages/AdaptersPage.tsx +39 -0
  127. package/packages/solidjs/src/pages/index.ts +2 -0
  128. package/packages/solidjs/src/primitives/index.ts +78 -0
  129. package/packages/solidjs/src/provider/AdapterProvider.tsx +62 -0
  130. package/packages/solidjs/src/provider/index.ts +2 -0
  131. package/packages/solidjs/tsconfig.json +20 -0
  132. package/packages/solidjs/tsup.config.ts +10 -0
  133. package/packages/solidjs-css/README.md +1 -0
  134. package/packages/solidjs-css/package.json +43 -0
  135. package/packages/solidjs-css/src/adapters.css +1576 -0
  136. package/packages/solidjs-css/src/components/AdapterCard.tsx +43 -0
  137. package/packages/solidjs-css/src/components/AdapterConfigForm.tsx +119 -0
  138. package/packages/solidjs-css/src/components/AdapterList.tsx +68 -0
  139. package/packages/solidjs-css/src/components/AdapterStatusBadge.tsx +24 -0
  140. package/packages/solidjs-css/src/components/index.ts +8 -0
  141. package/packages/solidjs-css/src/index.tsx +30 -0
  142. package/packages/solidjs-css/src/pages/AdapterDetailPage.tsx +107 -0
  143. package/packages/solidjs-css/src/pages/AdaptersPage.tsx +94 -0
  144. package/packages/solidjs-css/src/pages/index.ts +4 -0
  145. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  146. package/packages/solidjs-css/src/provider/AdapterProvider.tsx +61 -0
  147. package/packages/solidjs-css/src/provider/index.ts +2 -0
  148. package/packages/solidjs-css/tsconfig.json +20 -0
  149. package/packages/solidjs-css/tsup.config.ts +2 -0
  150. package/pnpm-workspace.yaml +2 -0
  151. package/tsconfig.json +17 -0
@@ -0,0 +1,120 @@
1
+ // @geenius/adapters — OpenAI implementation (MVP tier)
2
+ // Wraps the OpenAI API to conform to AiAdapter interface.
3
+ // Requires: openai
4
+
5
+ import type { ChatMessage, ChatResponse, AiOptions } from '../types'
6
+ import type { AiAdapter } from './interface'
7
+
8
+ interface OpenAIStreamChunk {
9
+ choices: Array<{ delta: { content?: string | null }; finish_reason: string | null }>
10
+ }
11
+
12
+ interface OpenAIClient {
13
+ chat: {
14
+ completions: {
15
+ create(params: {
16
+ model: string
17
+ messages: Array<{ role: string; content: string }>
18
+ temperature?: number
19
+ max_tokens?: number
20
+ stream?: false
21
+ }): Promise<{
22
+ choices: Array<{
23
+ message: { content: string | null }
24
+ finish_reason: string
25
+ }>
26
+ usage?: {
27
+ prompt_tokens: number
28
+ completion_tokens: number
29
+ total_tokens: number
30
+ }
31
+ }>
32
+ create(params: {
33
+ model: string
34
+ messages: Array<{ role: string; content: string }>
35
+ temperature?: number
36
+ max_tokens?: number
37
+ stream: true
38
+ }): Promise<AsyncIterable<OpenAIStreamChunk>>
39
+ }
40
+ }
41
+ embeddings: {
42
+ create: (params: {
43
+ model: string
44
+ input: string | string[]
45
+ }) => Promise<{
46
+ data: Array<{ embedding: number[] }>
47
+ }>
48
+ }
49
+ }
50
+
51
+ export interface OpenAiAdapterOptions {
52
+ /** Pre-configured OpenAI client instance */
53
+ client: OpenAIClient
54
+ /** Default model for chat/completions (default: gpt-4o-mini) */
55
+ defaultModel?: string
56
+ /** Default model for embeddings (default: text-embedding-3-small) */
57
+ embeddingModel?: string
58
+ }
59
+
60
+ export function createOpenAiAdapter(options: OpenAiAdapterOptions): AiAdapter {
61
+ const {
62
+ client,
63
+ defaultModel = 'gpt-4o-mini',
64
+ embeddingModel = 'text-embedding-3-small',
65
+ } = options
66
+
67
+ return {
68
+ async chat(messages: ChatMessage[], opts?: AiOptions): Promise<ChatResponse> {
69
+ const response = await client.chat.completions.create({
70
+ model: opts?.model || defaultModel,
71
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
72
+ temperature: opts?.temperature,
73
+ max_tokens: opts?.maxTokens,
74
+ })
75
+
76
+ const choice = response.choices[0]
77
+ return {
78
+ content: choice?.message?.content || '',
79
+ finishReason: (choice?.finish_reason === 'stop' ? 'stop'
80
+ : choice?.finish_reason === 'length' ? 'length'
81
+ : 'error') as ChatResponse['finishReason'],
82
+ usage: response.usage ? {
83
+ promptTokens: response.usage.prompt_tokens,
84
+ completionTokens: response.usage.completion_tokens,
85
+ totalTokens: response.usage.total_tokens,
86
+ } : undefined,
87
+ }
88
+ },
89
+
90
+ async complete(prompt: string, opts?: AiOptions): Promise<string> {
91
+ const response = await this.chat(
92
+ [{ role: 'user', content: prompt }],
93
+ opts,
94
+ )
95
+ return response.content
96
+ },
97
+
98
+ async embed(text: string | string[]): Promise<number[][]> {
99
+ const response = await client.embeddings.create({
100
+ model: embeddingModel,
101
+ input: text,
102
+ })
103
+ return response.data.map((d) => d.embedding)
104
+ },
105
+
106
+ async *stream(messages: ChatMessage[], opts?: AiOptions): AsyncIterable<string> {
107
+ const iterable = await client.chat.completions.create({
108
+ model: opts?.model || defaultModel,
109
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
110
+ temperature: opts?.temperature,
111
+ max_tokens: opts?.maxTokens,
112
+ stream: true,
113
+ })
114
+ for await (const chunk of iterable) {
115
+ const delta = chunk.choices[0]?.delta?.content
116
+ if (delta) yield delta
117
+ }
118
+ },
119
+ }
120
+ }
@@ -0,0 +1,101 @@
1
+ // @geenius/adapters — Vercel AI SDK adapter
2
+ // Wraps the Vercel AI SDK's generateText / embed to conform to AiAdapter.
3
+ // Requires: ai (Vercel AI SDK)
4
+
5
+ import type { ChatMessage, ChatResponse, AiOptions } from '../types'
6
+ import type { AiAdapter } from './interface'
7
+
8
+ export interface VercelAiAdapterOptions {
9
+ /** Vercel AI SDK model instance (e.g. openai('gpt-4o'), anthropic('claude-sonnet-4-20250514')) */
10
+ model: any
11
+ /** Optional embedding model instance */
12
+ embeddingModel?: any
13
+ }
14
+
15
+ export function createVercelAiAdapter(options: VercelAiAdapterOptions): AiAdapter {
16
+ const { model, embeddingModel } = options
17
+
18
+ return {
19
+ async chat(messages: ChatMessage[], opts?: AiOptions): Promise<ChatResponse> {
20
+ const { generateText } = await import('ai')
21
+
22
+ // Build messages in Vercel AI SDK format
23
+ const sdkMessages = messages.map((m) => ({
24
+ role: m.role as 'system' | 'user' | 'assistant',
25
+ content: m.content,
26
+ }))
27
+
28
+ const result = await generateText({
29
+ model: opts?.model ? model : model, // Use provided model instance
30
+ messages: sdkMessages,
31
+ temperature: opts?.temperature,
32
+ maxTokens: opts?.maxTokens,
33
+ })
34
+
35
+ return {
36
+ content: result.text || '',
37
+ finishReason: (result.finishReason === 'stop' ? 'stop'
38
+ : result.finishReason === 'length' ? 'length'
39
+ : 'error') as ChatResponse['finishReason'],
40
+ usage: result.usage ? {
41
+ promptTokens: result.usage.promptTokens || 0,
42
+ completionTokens: result.usage.completionTokens || 0,
43
+ totalTokens: (result.usage.promptTokens || 0) + (result.usage.completionTokens || 0),
44
+ } : undefined,
45
+ }
46
+ },
47
+
48
+ async complete(prompt: string, opts?: AiOptions): Promise<string> {
49
+ const { generateText } = await import('ai')
50
+
51
+ const result = await generateText({
52
+ model,
53
+ prompt,
54
+ temperature: opts?.temperature,
55
+ maxTokens: opts?.maxTokens,
56
+ })
57
+
58
+ return result.text || ''
59
+ },
60
+
61
+ async embed(text: string | string[]): Promise<number[][]> {
62
+ if (!embeddingModel) {
63
+ throw new Error(
64
+ 'Vercel AI SDK adapter requires an embeddingModel option for embeddings. ' +
65
+ 'Pass it when creating the adapter: createVercelAiAdapter({ model, embeddingModel })',
66
+ )
67
+ }
68
+
69
+ const { embed, embedMany } = await import('ai')
70
+ const inputs = Array.isArray(text) ? text : [text]
71
+
72
+ if (inputs.length === 1) {
73
+ const result = await embed({ model: embeddingModel, value: inputs[0] })
74
+ return [result.embedding]
75
+ }
76
+
77
+ const result = await embedMany({ model: embeddingModel, values: inputs })
78
+ return result.embeddings
79
+ },
80
+
81
+ async *stream(messages: ChatMessage[], opts?: AiOptions): AsyncIterable<string> {
82
+ const { streamText } = await import('ai')
83
+
84
+ const sdkMessages = messages.map((m) => ({
85
+ role: m.role as 'system' | 'user' | 'assistant',
86
+ content: m.content,
87
+ }))
88
+
89
+ const result = await streamText({
90
+ model,
91
+ messages: sdkMessages,
92
+ temperature: opts?.temperature,
93
+ maxTokens: opts?.maxTokens,
94
+ })
95
+
96
+ for await (const delta of result.textStream) {
97
+ yield delta
98
+ }
99
+ },
100
+ }
101
+ }
@@ -0,0 +1,118 @@
1
+ // @geenius/adapters — Better Auth implementation (Lancio tier)
2
+ // Wraps the Better Auth client SDK to conform to AuthAdapter interface.
3
+ // Requires: better-auth, @convex-dev/better-auth
4
+
5
+ import type { AuthUser, AuthSession } from '../types'
6
+ import type { AuthAdapter, OAuthProvider } from './interface'
7
+
8
+ interface BetterAuthClient {
9
+ signIn: {
10
+ email: (params: { email: string; password: string }) => Promise<{ data?: { session?: { token: string }; user?: { id: string; email: string; name: string; image?: string } }; error?: { message: string } }>
11
+ social: (params: { provider: string; callbackURL?: string }) => Promise<{ data?: { url?: string; redirect?: boolean }; error?: { message: string } }>
12
+ }
13
+ signUp: {
14
+ email: (params: { email: string; password: string; name: string }) => Promise<{ data?: { session?: { token: string }; user?: { id: string; email: string; name: string; image?: string } }; error?: { message: string } }>
15
+ }
16
+ signOut: () => Promise<void>
17
+ getSession: () => Promise<{ data?: { session?: { token: string; expiresAt: string; userId: string }; user?: { id: string; email: string; name: string; image?: string } } }>
18
+ updateUser: (params: { name?: string; image?: string }) => Promise<{ data?: { id: string; email: string; name: string; image?: string }; error?: { message: string } }>
19
+ }
20
+
21
+ export interface BetterAuthAdapterOptions {
22
+ /** Pre-configured Better Auth client instance */
23
+ client: BetterAuthClient
24
+ }
25
+
26
+ export function createBetterAuthAdapter(options: BetterAuthAdapterOptions): AuthAdapter {
27
+ const { client } = options
28
+
29
+ return {
30
+ async signInWithOAuth(provider: OAuthProvider, options?: { redirectUrl?: string }): Promise<{ url: string }> {
31
+ const result = await client.signIn.social({
32
+ provider,
33
+ callbackURL: options?.redirectUrl,
34
+ })
35
+ if (result.error) throw new Error(result.error.message)
36
+ const url = result.data?.url ?? options?.redirectUrl ?? '/'
37
+ return { url }
38
+ },
39
+
40
+ async signIn(email: string, password: string): Promise<AuthSession> {
41
+ const result = await client.signIn.email({ email, password })
42
+ if (result.error) throw new Error(result.error.message)
43
+ if (!result.data?.session || !result.data?.user) throw new Error('Sign in failed')
44
+
45
+ return {
46
+ userId: result.data.user.id,
47
+ token: result.data.session.token,
48
+ }
49
+ },
50
+
51
+ async signUp(email: string, password: string, name?: string): Promise<AuthSession> {
52
+ const result = await client.signUp.email({
53
+ email,
54
+ password,
55
+ name: name || email.split('@')[0],
56
+ })
57
+ if (result.error) throw new Error(result.error.message)
58
+ if (!result.data?.session || !result.data?.user) throw new Error('Sign up failed')
59
+
60
+ return {
61
+ userId: result.data.user.id,
62
+ token: result.data.session.token,
63
+ }
64
+ },
65
+
66
+ async signOut(): Promise<void> {
67
+ await client.signOut()
68
+ },
69
+
70
+ async getSession(): Promise<AuthSession | null> {
71
+ try {
72
+ const result = await client.getSession()
73
+ if (!result.data?.session) return null
74
+
75
+ return {
76
+ userId: result.data.session.userId,
77
+ token: result.data.session.token,
78
+ expiresAt: result.data.session.expiresAt,
79
+ }
80
+ } catch {
81
+ return null
82
+ }
83
+ },
84
+
85
+ async getUser(): Promise<AuthUser | null> {
86
+ try {
87
+ const result = await client.getSession()
88
+ if (!result.data?.user) return null
89
+
90
+ return {
91
+ id: result.data.user.id,
92
+ email: result.data.user.email,
93
+ name: result.data.user.name,
94
+ image: result.data.user.image,
95
+ }
96
+ } catch {
97
+ return null
98
+ }
99
+ },
100
+
101
+ async updateUser(updates: Partial<Pick<AuthUser, 'name' | 'image'>>): Promise<AuthUser | null> {
102
+ try {
103
+ const result = await client.updateUser(updates)
104
+ if (result.error) throw new Error(result.error.message)
105
+ if (!result.data) return null
106
+
107
+ return {
108
+ id: result.data.id,
109
+ email: result.data.email,
110
+ name: result.data.name,
111
+ image: result.data.image,
112
+ }
113
+ } catch {
114
+ return null
115
+ }
116
+ },
117
+ }
118
+ }
@@ -0,0 +1,151 @@
1
+ // @geenius/adapters — Clerk auth adapter
2
+ // Wraps the Clerk client SDK to conform to AuthAdapter interface.
3
+ // Requires: @clerk/clerk-js or @clerk/clerk-react
4
+
5
+ import type { AuthUser, AuthSession } from '../types'
6
+ import type { AuthAdapter, OAuthProvider } from './interface'
7
+
8
+ interface ClerkClient {
9
+ signIn: {
10
+ create: (params: { identifier: string; password: string }) => Promise<{
11
+ status: string
12
+ createdSessionId: string | null
13
+ }>
14
+ authenticateWithRedirect: (params: {
15
+ strategy: string
16
+ redirectUrl: string
17
+ redirectUrlComplete: string
18
+ }) => Promise<void>
19
+ }
20
+ signUp: {
21
+ create: (params: { emailAddress: string; password: string; firstName?: string }) => Promise<{
22
+ status: string
23
+ createdSessionId: string | null
24
+ }>
25
+ }
26
+ signOut: () => Promise<void>
27
+ session: {
28
+ getToken: () => Promise<string | null>
29
+ } | null
30
+ user: {
31
+ id: string
32
+ primaryEmailAddress: { emailAddress: string } | null
33
+ firstName: string | null
34
+ lastName: string | null
35
+ imageUrl: string
36
+ update: (params: { firstName?: string; lastName?: string }) => Promise<any>
37
+ } | null
38
+ }
39
+
40
+ export interface ClerkAuthAdapterOptions {
41
+ /** Pre-configured Clerk client instance (e.g. from useClerk() or Clerk.load()) */
42
+ client: ClerkClient
43
+ }
44
+
45
+ export function createClerkAuthAdapter(options: ClerkAuthAdapterOptions): AuthAdapter {
46
+ const { client } = options
47
+
48
+ return {
49
+ async signInWithOAuth(provider: OAuthProvider, options?: { redirectUrl?: string }): Promise<{ url: string }> {
50
+ // Clerk's OAuth flow is browser-redirect based. We initiate the redirect;
51
+ // the browser navigates to the provider. The returned URL is where Clerk will
52
+ // redirect back to after authentication.
53
+ const redirectUrlComplete = options?.redirectUrl ?? window.location.origin
54
+ const clerkCallbackUrl = `${window.location.origin}/sign-in/sso-callback`
55
+ await client.signIn.authenticateWithRedirect({
56
+ strategy: `oauth_${provider}`,
57
+ redirectUrl: clerkCallbackUrl,
58
+ redirectUrlComplete,
59
+ })
60
+ // authenticateWithRedirect triggers a browser navigation; this return is nominal.
61
+ return { url: clerkCallbackUrl }
62
+ },
63
+
64
+ async signIn(email: string, password: string): Promise<AuthSession> {
65
+ const result = await client.signIn.create({ identifier: email, password })
66
+ if (result.status !== 'complete' || !result.createdSessionId) {
67
+ throw new Error('Clerk sign in failed — status: ' + result.status)
68
+ }
69
+
70
+ const token = await client.session?.getToken() || result.createdSessionId
71
+
72
+ return {
73
+ userId: client.user?.id || '',
74
+ token,
75
+ }
76
+ },
77
+
78
+ async signUp(email: string, password: string, name?: string): Promise<AuthSession> {
79
+ const result = await client.signUp.create({
80
+ emailAddress: email,
81
+ password,
82
+ firstName: name || email.split('@')[0],
83
+ })
84
+ if (result.status !== 'complete' || !result.createdSessionId) {
85
+ throw new Error('Clerk sign up failed — status: ' + result.status)
86
+ }
87
+
88
+ const token = await client.session?.getToken() || result.createdSessionId
89
+
90
+ return {
91
+ userId: client.user?.id || '',
92
+ token,
93
+ }
94
+ },
95
+
96
+ async signOut(): Promise<void> {
97
+ await client.signOut()
98
+ },
99
+
100
+ async getSession(): Promise<AuthSession | null> {
101
+ try {
102
+ if (!client.session) return null
103
+ const token = await client.session.getToken()
104
+ if (!token) return null
105
+
106
+ return {
107
+ userId: client.user?.id || '',
108
+ token,
109
+ }
110
+ } catch {
111
+ return null
112
+ }
113
+ },
114
+
115
+ async getUser(): Promise<AuthUser | null> {
116
+ try {
117
+ if (!client.user) return null
118
+
119
+ const fullName = [client.user.firstName, client.user.lastName].filter(Boolean).join(' ')
120
+
121
+ return {
122
+ id: client.user.id,
123
+ email: client.user.primaryEmailAddress?.emailAddress || '',
124
+ name: fullName || undefined,
125
+ image: client.user.imageUrl || undefined,
126
+ }
127
+ } catch {
128
+ return null
129
+ }
130
+ },
131
+
132
+ async updateUser(updates: Partial<Pick<AuthUser, 'name' | 'image'>>): Promise<AuthUser | null> {
133
+ try {
134
+ if (!client.user) return null
135
+
136
+ const params: Record<string, string> = {}
137
+ if (updates.name) {
138
+ const parts = updates.name.split(' ')
139
+ params.firstName = parts[0]
140
+ if (parts.length > 1) params.lastName = parts.slice(1).join(' ')
141
+ }
142
+
143
+ await client.user.update(params)
144
+
145
+ return this.getUser()
146
+ } catch {
147
+ return null
148
+ }
149
+ },
150
+ }
151
+ }
@@ -0,0 +1,125 @@
1
+ // @geenius/adapters — Convex Auth implementation (MVP tier)
2
+ // Wraps @convex-dev/auth client hooks to conform to AuthAdapter interface.
3
+ // Requires: convex, @convex-dev/auth
4
+
5
+ import type { AuthUser, AuthSession } from '../types'
6
+ import type { AuthAdapter, OAuthProvider } from './interface'
7
+
8
+ interface ConvexAuthClient {
9
+ signIn: (provider: string, params: Record<string, string>) => Promise<void>
10
+ signOut: () => Promise<void>
11
+ isAuthenticated: () => boolean
12
+ isLoading: () => boolean
13
+ }
14
+
15
+ interface ConvexQueryClient {
16
+ query: (fn: any, args?: any) => Promise<any>
17
+ mutation: (fn: any, args?: any) => Promise<any>
18
+ }
19
+
20
+ export interface ConvexAuthAdapterOptions {
21
+ /** The Convex Auth client (from useConvexAuth or equivalent) */
22
+ authClient: ConvexAuthClient
23
+ /** The Convex client for running queries/mutations */
24
+ convexClient: ConvexQueryClient
25
+ /** The query function reference for getting current user */
26
+ getCurrentUserQuery: any
27
+ /** The mutation function reference for updating user */
28
+ updateUserMutation?: any
29
+ }
30
+
31
+ export function createConvexAuthAdapter(options: ConvexAuthAdapterOptions): AuthAdapter {
32
+ const { authClient, convexClient, getCurrentUserQuery, updateUserMutation } = options
33
+
34
+ return {
35
+ async signInWithOAuth(provider: OAuthProvider, options?: { redirectUrl?: string }): Promise<{ url: string }> {
36
+ // Convex Auth uses the same signIn() entry point for social providers.
37
+ // The provider name must match what's configured in the Convex Auth config.
38
+ await authClient.signIn(provider, {
39
+ redirectTo: options?.redirectUrl,
40
+ })
41
+ // Convex Auth manages the redirect internally; the return URL is nominal.
42
+ return { url: options?.redirectUrl ?? '/' }
43
+ },
44
+
45
+ async signIn(email: string, password: string): Promise<AuthSession> {
46
+ await authClient.signIn('password', { email, password, flow: 'signIn' })
47
+
48
+ // Convex Auth manages sessions internally via cookies
49
+ // We return a placeholder session — the real session is managed by Convex
50
+ return {
51
+ userId: '', // Will be populated by getUser()
52
+ token: 'convex-managed',
53
+ }
54
+ },
55
+
56
+ async signUp(email: string, password: string, name?: string): Promise<AuthSession> {
57
+ await authClient.signIn('password', {
58
+ email,
59
+ password,
60
+ name: name || email.split('@')[0],
61
+ flow: 'signUp',
62
+ })
63
+
64
+ return {
65
+ userId: '',
66
+ token: 'convex-managed',
67
+ }
68
+ },
69
+
70
+ async signOut(): Promise<void> {
71
+ await authClient.signOut()
72
+ },
73
+
74
+ async getSession(): Promise<AuthSession | null> {
75
+ if (!authClient.isAuthenticated()) return null
76
+
77
+ try {
78
+ const user = await convexClient.query(getCurrentUserQuery)
79
+ if (!user) return null
80
+
81
+ return {
82
+ userId: user._id || user.id,
83
+ token: 'convex-managed',
84
+ }
85
+ } catch {
86
+ return null
87
+ }
88
+ },
89
+
90
+ async getUser(): Promise<AuthUser | null> {
91
+ if (!authClient.isAuthenticated()) return null
92
+
93
+ try {
94
+ const user = await convexClient.query(getCurrentUserQuery)
95
+ if (!user) return null
96
+
97
+ return {
98
+ id: user._id || user.id,
99
+ email: user.email,
100
+ name: user.name,
101
+ image: user.image,
102
+ role: user.role,
103
+ createdAt: user._creationTime
104
+ ? new Date(user._creationTime).toISOString()
105
+ : undefined,
106
+ }
107
+ } catch {
108
+ return null
109
+ }
110
+ },
111
+
112
+ async updateUser(updates: Partial<Pick<AuthUser, 'name' | 'image'>>): Promise<AuthUser | null> {
113
+ if (!updateUserMutation) {
114
+ throw new Error('updateUserMutation not configured')
115
+ }
116
+
117
+ try {
118
+ await convexClient.mutation(updateUserMutation, updates)
119
+ return this.getUser()
120
+ } catch {
121
+ return null
122
+ }
123
+ },
124
+ }
125
+ }
@@ -0,0 +1,10 @@
1
+ export type { AuthAdapter, OAuthProvider } from './interface'
2
+ export { createLocalStorageAuthAdapter } from './localStorage'
3
+ export { createBetterAuthAdapter } from './better-auth'
4
+ export type { BetterAuthAdapterOptions } from './better-auth'
5
+ export { createConvexAuthAdapter } from './convex-auth'
6
+ export type { ConvexAuthAdapterOptions } from './convex-auth'
7
+ export { createClerkAuthAdapter } from './clerk'
8
+ export type { ClerkAuthAdapterOptions } from './clerk'
9
+ export { createSupabaseAuthAdapter } from './supabase-auth'
10
+ export type { SupabaseAuthAdapterOptions } from './supabase-auth'
@@ -0,0 +1,17 @@
1
+ // @geenius/adapters — Auth adapter interface
2
+
3
+ import type { AuthUser, AuthSession } from '../types'
4
+
5
+ /** OAuth provider identifiers */
6
+ export type OAuthProvider = 'google' | 'github' | 'discord' | 'twitter' | 'apple' | 'microsoft' | string
7
+
8
+ export interface AuthAdapter {
9
+ signIn(email: string, password: string): Promise<AuthSession>
10
+ signUp(email: string, password: string, name?: string): Promise<AuthSession>
11
+ /** Initiate an OAuth sign-in flow. Returns the redirect URL for the provider. */
12
+ signInWithOAuth(provider: OAuthProvider, options?: { redirectUrl?: string }): Promise<{ url: string }>
13
+ signOut(): Promise<void>
14
+ getSession(): Promise<AuthSession | null>
15
+ getUser(): Promise<AuthUser | null>
16
+ updateUser(updates: Partial<Pick<AuthUser, 'name' | 'image'>>): Promise<AuthUser | null>
17
+ }