@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.
- package/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +11 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +51 -0
- package/packages/convex/README.md +64 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/adapter.ts +39 -0
- package/packages/convex/src/index.ts +19 -0
- package/packages/convex/src/mutations.ts +142 -0
- package/packages/convex/src/queries.ts +106 -0
- package/packages/convex/src/schema.ts +54 -0
- package/packages/convex/src/types.ts +20 -0
- package/packages/convex/tsconfig.json +11 -0
- package/packages/convex/tsup.config.ts +10 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +45 -0
- package/packages/react/src/components/AdapterCard.tsx +49 -0
- package/packages/react/src/components/AdapterConfigForm.tsx +118 -0
- package/packages/react/src/components/AdapterList.tsx +84 -0
- package/packages/react/src/components/AdapterStatusBadge.tsx +30 -0
- package/packages/react/src/components/index.ts +4 -0
- package/packages/react/src/hooks/index.ts +75 -0
- package/packages/react/src/index.tsx +44 -0
- package/packages/react/src/pages/AdapterDetailPage.tsx +133 -0
- package/packages/react/src/pages/AdaptersPage.tsx +111 -0
- package/packages/react/src/pages/index.ts +2 -0
- package/packages/react/src/provider/AdapterProvider.tsx +115 -0
- package/packages/react/src/provider/index.ts +2 -0
- package/packages/react/tsconfig.json +18 -0
- package/packages/react/tsup.config.ts +10 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +44 -0
- package/packages/react-css/src/adapters.css +1576 -0
- package/packages/react-css/src/components/AdapterCard.tsx +34 -0
- package/packages/react-css/src/components/AdapterConfigForm.tsx +63 -0
- package/packages/react-css/src/components/AdapterList.tsx +40 -0
- package/packages/react-css/src/components/AdapterStatusBadge.tsx +21 -0
- package/packages/react-css/src/components/index.ts +4 -0
- package/packages/react-css/src/hooks/index.ts +75 -0
- package/packages/react-css/src/index.tsx +25 -0
- package/packages/react-css/src/pages/AdapterDetailPage.tsx +133 -0
- package/packages/react-css/src/pages/AdaptersPage.tsx +111 -0
- package/packages/react-css/src/pages/index.ts +2 -0
- package/packages/react-css/src/provider/AdapterProvider.tsx +115 -0
- package/packages/react-css/src/provider/index.ts +2 -0
- package/packages/react-css/src/styles.css +494 -0
- package/packages/react-css/tsconfig.json +19 -0
- package/packages/react-css/tsup.config.ts +2 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +39 -0
- package/packages/shared/src/__tests__/adapters.test.ts +545 -0
- package/packages/shared/src/admin/index.ts +2 -0
- package/packages/shared/src/admin/interface.ts +34 -0
- package/packages/shared/src/admin/localStorage.ts +109 -0
- package/packages/shared/src/ai/anthropic.ts +123 -0
- package/packages/shared/src/ai/cloudflare-gateway.ts +130 -0
- package/packages/shared/src/ai/gemini.ts +181 -0
- package/packages/shared/src/ai/index.ts +14 -0
- package/packages/shared/src/ai/interface.ts +11 -0
- package/packages/shared/src/ai/localStorage.ts +78 -0
- package/packages/shared/src/ai/ollama.ts +143 -0
- package/packages/shared/src/ai/openai.ts +120 -0
- package/packages/shared/src/ai/vercel-ai.ts +101 -0
- package/packages/shared/src/auth/better-auth.ts +118 -0
- package/packages/shared/src/auth/clerk.ts +151 -0
- package/packages/shared/src/auth/convex-auth.ts +125 -0
- package/packages/shared/src/auth/index.ts +10 -0
- package/packages/shared/src/auth/interface.ts +17 -0
- package/packages/shared/src/auth/localStorage.ts +125 -0
- package/packages/shared/src/auth/supabase-auth.ts +136 -0
- package/packages/shared/src/config.ts +57 -0
- package/packages/shared/src/constants.ts +122 -0
- package/packages/shared/src/db/convex.ts +146 -0
- package/packages/shared/src/db/index.ts +10 -0
- package/packages/shared/src/db/interface.ts +13 -0
- package/packages/shared/src/db/localStorage.ts +91 -0
- package/packages/shared/src/db/mongodb.ts +125 -0
- package/packages/shared/src/db/neon.ts +171 -0
- package/packages/shared/src/db/supabase.ts +158 -0
- package/packages/shared/src/index.ts +117 -0
- package/packages/shared/src/payments/index.ts +4 -0
- package/packages/shared/src/payments/interface.ts +11 -0
- package/packages/shared/src/payments/localStorage.ts +81 -0
- package/packages/shared/src/payments/stripe.ts +177 -0
- package/packages/shared/src/storage/convex.ts +113 -0
- package/packages/shared/src/storage/index.ts +14 -0
- package/packages/shared/src/storage/interface.ts +11 -0
- package/packages/shared/src/storage/localStorage.ts +95 -0
- package/packages/shared/src/storage/minio.ts +47 -0
- package/packages/shared/src/storage/r2.ts +123 -0
- package/packages/shared/src/storage/s3.ts +128 -0
- package/packages/shared/src/storage/supabase-storage.ts +116 -0
- package/packages/shared/src/storage/uploadthing.ts +126 -0
- package/packages/shared/src/styles/adapters.css +494 -0
- package/packages/shared/src/tier-gate.ts +119 -0
- package/packages/shared/src/types.ts +162 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/shared/tsup.config.ts +9 -0
- package/packages/shared/vitest.config.ts +14 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +44 -0
- package/packages/solidjs/src/components/AdapterCard.tsx +24 -0
- package/packages/solidjs/src/components/AdapterConfigForm.tsx +54 -0
- package/packages/solidjs/src/components/AdapterList.tsx +28 -0
- package/packages/solidjs/src/components/AdapterStatusBadge.tsx +20 -0
- package/packages/solidjs/src/components/index.ts +4 -0
- package/packages/solidjs/src/index.tsx +17 -0
- package/packages/solidjs/src/pages/AdapterDetailPage.tsx +38 -0
- package/packages/solidjs/src/pages/AdaptersPage.tsx +39 -0
- package/packages/solidjs/src/pages/index.ts +2 -0
- package/packages/solidjs/src/primitives/index.ts +78 -0
- package/packages/solidjs/src/provider/AdapterProvider.tsx +62 -0
- package/packages/solidjs/src/provider/index.ts +2 -0
- package/packages/solidjs/tsconfig.json +20 -0
- package/packages/solidjs/tsup.config.ts +10 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +43 -0
- package/packages/solidjs-css/src/adapters.css +1576 -0
- package/packages/solidjs-css/src/components/AdapterCard.tsx +43 -0
- package/packages/solidjs-css/src/components/AdapterConfigForm.tsx +119 -0
- package/packages/solidjs-css/src/components/AdapterList.tsx +68 -0
- package/packages/solidjs-css/src/components/AdapterStatusBadge.tsx +24 -0
- package/packages/solidjs-css/src/components/index.ts +8 -0
- package/packages/solidjs-css/src/index.tsx +30 -0
- package/packages/solidjs-css/src/pages/AdapterDetailPage.tsx +107 -0
- package/packages/solidjs-css/src/pages/AdaptersPage.tsx +94 -0
- package/packages/solidjs-css/src/pages/index.ts +4 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/provider/AdapterProvider.tsx +61 -0
- package/packages/solidjs-css/src/provider/index.ts +2 -0
- package/packages/solidjs-css/tsconfig.json +20 -0
- package/packages/solidjs-css/tsup.config.ts +2 -0
- package/pnpm-workspace.yaml +2 -0
- 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
|
+
}
|