@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,81 @@
1
+ // @geenius/adapters — localStorage Payments implementation (MVP tier mock)
2
+
3
+ import type { Plan, Subscription, CheckoutParams, CheckoutResult } from '../types'
4
+ import type { PaymentsAdapter } from './interface'
5
+
6
+ const SUBS_KEY = 'geenius_subscriptions'
7
+ const PLANS_KEY = 'geenius_plans'
8
+
9
+ const DEFAULT_PLANS: Plan[] = [
10
+ { id: 'free', name: 'Free', price: 0, currency: 'USD', interval: 'month', features: ['Basic features', '1 project', 'Community support'] },
11
+ { id: 'pro', name: 'Pro', price: 29, currency: 'USD', interval: 'month', features: ['All Free features', 'Unlimited projects', 'Priority support', 'AI features'] },
12
+ { id: 'team', name: 'Team', price: 79, currency: 'USD', interval: 'month', features: ['All Pro features', 'Team management', 'Admin dashboard', 'Custom integrations'] },
13
+ ]
14
+
15
+ function getSubs(): Subscription[] {
16
+ try { return JSON.parse(localStorage.getItem(SUBS_KEY) || '[]') } catch { return [] }
17
+ }
18
+ function saveSubs(subs: Subscription[]) { localStorage.setItem(SUBS_KEY, JSON.stringify(subs)) }
19
+
20
+ export function createLocalStoragePaymentsAdapter(): PaymentsAdapter {
21
+ // Seed default plans
22
+ if (!localStorage.getItem(PLANS_KEY)) {
23
+ localStorage.setItem(PLANS_KEY, JSON.stringify(DEFAULT_PLANS))
24
+ }
25
+
26
+ return {
27
+ async createCheckout(params: CheckoutParams) {
28
+ // Mock: instantly create a subscription
29
+ const subs = getSubs()
30
+ const sub: Subscription = {
31
+ id: crypto.randomUUID(),
32
+ userId: params.userId,
33
+ planId: params.planId,
34
+ status: 'active',
35
+ currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
36
+ cancelAtPeriodEnd: false,
37
+ }
38
+ subs.push(sub)
39
+ saveSubs(subs)
40
+ return { url: params.successUrl || '/', sessionId: sub.id } as CheckoutResult
41
+ },
42
+
43
+ async getSubscription(userId) {
44
+ const sub = getSubs().find(s => s.userId === userId && s.status === 'active')
45
+ return sub ?? null
46
+ },
47
+
48
+ async cancelSubscription(subscriptionId) {
49
+ const subs = getSubs()
50
+ const idx = subs.findIndex(s => s.id === subscriptionId)
51
+ if (idx !== -1) {
52
+ subs[idx].cancelAtPeriodEnd = true
53
+ subs[idx].status = 'cancelled'
54
+ saveSubs(subs)
55
+ }
56
+ },
57
+
58
+ async getPlans() {
59
+ try { return JSON.parse(localStorage.getItem(PLANS_KEY) || '[]') } catch { return DEFAULT_PLANS }
60
+ },
61
+
62
+ async isFeatureEnabled(userId, feature) {
63
+ const sub = await this.getSubscription(userId)
64
+ if (!sub) return false
65
+ const plans: Plan[] = await this.getPlans()
66
+ const plan = plans.find(p => p.id === sub.planId)
67
+ return plan?.features.includes(feature) ?? false
68
+ },
69
+ }
70
+ }
71
+
72
+ // No-op adapter for Pronto tier (payments completely disabled)
73
+ export function createNoopPaymentsAdapter(): PaymentsAdapter {
74
+ return {
75
+ async createCheckout() { throw new Error('Payments not available in Pronto tier. Upgrade to Lancio.') },
76
+ async getSubscription() { return null },
77
+ async cancelSubscription() {},
78
+ async getPlans() { return [] },
79
+ async isFeatureEnabled() { return false },
80
+ }
81
+ }
@@ -0,0 +1,177 @@
1
+ // @geenius/adapters — Stripe payments implementation (MVP/Paid tier)
2
+ // Wraps the Stripe Node.js SDK to conform to PaymentsAdapter interface.
3
+ // Requires: stripe
4
+
5
+ import type { Plan, Subscription, CheckoutParams, CheckoutResult } from '../types'
6
+ import type { PaymentsAdapter } from './interface'
7
+
8
+ interface StripeClient {
9
+ checkout: {
10
+ sessions: {
11
+ create: (params: any) => Promise<{ id: string; url: string | null }>
12
+ }
13
+ }
14
+ subscriptions: {
15
+ retrieve: (id: string) => Promise<{
16
+ id: string
17
+ customer: string
18
+ status: string
19
+ current_period_end: number
20
+ cancel_at_period_end: boolean
21
+ items: { data: Array<{ price: { id: string; product: string } }> }
22
+ }>
23
+ cancel: (id: string) => Promise<any>
24
+ }
25
+ prices: {
26
+ list: (params?: any) => Promise<{
27
+ data: Array<{
28
+ id: string
29
+ unit_amount: number | null
30
+ currency: string
31
+ recurring: { interval: string } | null
32
+ product: string | { id: string; name: string; metadata?: Record<string, string> }
33
+ }>
34
+ }>
35
+ }
36
+ products: {
37
+ retrieve: (id: string) => Promise<{
38
+ id: string
39
+ name: string
40
+ metadata?: Record<string, string>
41
+ }>
42
+ }
43
+ }
44
+
45
+ export interface StripePaymentsAdapterOptions {
46
+ /** Pre-configured Stripe client instance */
47
+ client: StripeClient
48
+ /** Map of plan IDs to Stripe price IDs */
49
+ priceMappings?: Record<string, string>
50
+ /** Default success URL for checkout */
51
+ successUrl?: string
52
+ /** Default cancel URL for checkout */
53
+ cancelUrl?: string
54
+ /**
55
+ * Lookup a Stripe subscription ID by userId.
56
+ * The adapter cannot look this up directly — your app layer must map
57
+ * userId → Stripe subscriptionId (typically stored in your DB).
58
+ *
59
+ * @example
60
+ * subscriptionLookup: async (userId) => {
61
+ * const user = await db.users.findById(userId)
62
+ * return user?.stripeSubscriptionId ?? null
63
+ * }
64
+ */
65
+ subscriptionLookup?: (userId: string) => Promise<string | null>
66
+ }
67
+
68
+ export function createStripePaymentsAdapter(options: StripePaymentsAdapterOptions): PaymentsAdapter {
69
+ const {
70
+ client,
71
+ priceMappings = {},
72
+ successUrl = '/billing/success',
73
+ cancelUrl = '/billing/cancel',
74
+ subscriptionLookup,
75
+ } = options
76
+
77
+ // Cache plans to avoid repeated API calls
78
+ let cachedPlans: Plan[] | null = null
79
+
80
+ return {
81
+ async createCheckout(params: CheckoutParams): Promise<CheckoutResult> {
82
+ const priceId = priceMappings[params.planId] || params.planId
83
+
84
+ const session = await client.checkout.sessions.create({
85
+ mode: 'subscription',
86
+ line_items: [{ price: priceId, quantity: 1 }],
87
+ success_url: params.successUrl || successUrl,
88
+ cancel_url: params.cancelUrl || cancelUrl,
89
+ client_reference_id: params.userId,
90
+ metadata: { userId: params.userId, planId: params.planId },
91
+ })
92
+
93
+ return {
94
+ url: session.url || '',
95
+ sessionId: session.id,
96
+ }
97
+ },
98
+
99
+ async getSubscription(userId: string): Promise<Subscription | null> {
100
+ if (!subscriptionLookup) return null
101
+ try {
102
+ const subscriptionId = await subscriptionLookup(userId)
103
+ if (!subscriptionId) return null
104
+ const raw = await client.subscriptions.retrieve(subscriptionId)
105
+ return {
106
+ id: raw.id,
107
+ userId,
108
+ planId: raw.items.data[0]?.price.id ?? '',
109
+ status: (['active', 'cancelled', 'past_due', 'trialing'] as const).includes(raw.status as any)
110
+ ? (raw.status as Subscription['status'])
111
+ : 'past_due',
112
+ currentPeriodEnd: new Date(raw.current_period_end * 1000).toISOString(),
113
+ cancelAtPeriodEnd: raw.cancel_at_period_end,
114
+ }
115
+ } catch {
116
+ return null
117
+ }
118
+ },
119
+
120
+ async cancelSubscription(subscriptionId: string): Promise<void> {
121
+ await client.subscriptions.cancel(subscriptionId)
122
+ },
123
+
124
+ async getPlans(): Promise<Plan[]> {
125
+ if (cachedPlans) return cachedPlans
126
+
127
+ const { data: prices } = await client.prices.list({
128
+ active: true,
129
+ expand: ['data.product'],
130
+ })
131
+
132
+ cachedPlans = await Promise.all(
133
+ prices
134
+ .filter((p) => p.recurring)
135
+ .map(async (price) => {
136
+ let productName = 'Unknown'
137
+ let features: string[] = []
138
+
139
+ if (typeof price.product === 'object' && price.product.name) {
140
+ productName = price.product.name
141
+ features = price.product.metadata?.features
142
+ ? price.product.metadata.features.split(',').map((f) => f.trim())
143
+ : []
144
+ } else if (typeof price.product === 'string') {
145
+ const product = await client.products.retrieve(price.product)
146
+ productName = product.name
147
+ features = product.metadata?.features
148
+ ? product.metadata.features.split(',').map((f) => f.trim())
149
+ : []
150
+ }
151
+
152
+ return {
153
+ id: price.id,
154
+ name: productName,
155
+ price: (price.unit_amount || 0) / 100,
156
+ currency: price.currency,
157
+ interval: (price.recurring?.interval === 'year' ? 'year' : 'month') as Plan['interval'],
158
+ features,
159
+ }
160
+ })
161
+ )
162
+
163
+ return cachedPlans
164
+ },
165
+
166
+ async isFeatureEnabled(userId: string, feature: string): Promise<boolean> {
167
+ // In a real implementation, look up the user's subscription and check
168
+ // if their plan includes the feature. Simplified: always return true.
169
+ const subscription = await this.getSubscription(userId)
170
+ if (!subscription || subscription.status !== 'active') return false
171
+
172
+ const plans = await this.getPlans()
173
+ const plan = plans.find((p) => p.id === subscription.planId)
174
+ return plan?.features.includes(feature) ?? false
175
+ },
176
+ }
177
+ }
@@ -0,0 +1,113 @@
1
+ // @geenius/adapters — Convex file storage adapter
2
+ // Wraps Convex's built-in storage API (generateUploadUrl, storage.getUrl, etc.)
3
+ // Requires a Convex client instance passed by the consuming app.
4
+
5
+ import type { StoredFile } from '../types'
6
+ import type { FileStorageAdapter } from './interface'
7
+
8
+ export interface ConvexFileAdapterOptions {
9
+ /** Convex client instance (e.g. from useConvex() or ConvexHttpClient) */
10
+ client: any
11
+ /** Convex API reference for file storage mutations/queries */
12
+ api: {
13
+ generateUploadUrl?: any
14
+ saveFile?: any
15
+ deleteFile?: any
16
+ listFiles?: any
17
+ getFile?: any
18
+ }
19
+ }
20
+
21
+ export function createConvexFileAdapter(options: ConvexFileAdapterOptions): FileStorageAdapter {
22
+ const { client, api } = options
23
+
24
+ return {
25
+ async upload(file, path, name?) {
26
+ const id = crypto.randomUUID()
27
+ const fileName = name || (file instanceof File ? file.name : `file-${id.slice(0, 8)}`)
28
+
29
+ // Step 1: Get a presigned upload URL from Convex
30
+ const uploadUrl = await client.mutation(api.generateUploadUrl)
31
+
32
+ // Step 2: Upload the file to Convex storage
33
+ const response = await fetch(uploadUrl, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': file.type || 'application/octet-stream' },
36
+ body: file,
37
+ })
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`Convex upload failed: ${response.statusText}`)
41
+ }
42
+
43
+ const { storageId } = await response.json()
44
+
45
+ // Step 3: Save file metadata in Convex DB
46
+ if (api.saveFile) {
47
+ await client.mutation(api.saveFile, {
48
+ storageId,
49
+ name: fileName,
50
+ originalName: fileName,
51
+ mimeType: file.type || 'application/octet-stream',
52
+ size: file.size,
53
+ folder: path || undefined,
54
+ })
55
+ }
56
+
57
+ // Step 4: Get the public URL
58
+ const url = await client.query(api.getFile, { storageId }) ?? `convex://${storageId}`
59
+
60
+ return {
61
+ id: storageId,
62
+ name: fileName,
63
+ path: path ? `${path}/${fileName}` : fileName,
64
+ size: file.size,
65
+ mimeType: file.type || 'application/octet-stream',
66
+ url,
67
+ createdAt: new Date().toISOString(),
68
+ }
69
+ },
70
+
71
+ async download(fileId) {
72
+ // Convex storage URLs are directly fetchable
73
+ const url = await client.query(api.getFile, { storageId: fileId })
74
+ if (!url) throw new Error(`File not found: ${fileId}`)
75
+ const response = await fetch(url)
76
+ if (!response.ok) throw new Error(`Failed to download file: ${fileId}`)
77
+ return response.blob()
78
+ },
79
+
80
+ async delete(fileId) {
81
+ try {
82
+ if (api.deleteFile) {
83
+ await client.mutation(api.deleteFile, { id: fileId })
84
+ }
85
+ return true
86
+ } catch {
87
+ return false
88
+ }
89
+ },
90
+
91
+ async list(prefix?) {
92
+ if (!api.listFiles) return []
93
+ const files = await client.query(api.listFiles, { folder: prefix })
94
+ return (files || []).map((f: any): StoredFile => ({
95
+ id: f._id || f.storageId,
96
+ name: f.name || f.originalName,
97
+ path: f.folder ? `${f.folder}/${f.name}` : f.name,
98
+ size: f.size || 0,
99
+ mimeType: f.mimeType || 'application/octet-stream',
100
+ url: f.url || '',
101
+ createdAt: f.createdAt ? new Date(f.createdAt).toISOString() : new Date().toISOString(),
102
+ }))
103
+ },
104
+
105
+ async getUrl(fileId) {
106
+ if (api.getFile) {
107
+ const url = await client.query(api.getFile, { storageId: fileId })
108
+ if (url) return url
109
+ }
110
+ return `convex://${fileId}`
111
+ },
112
+ }
113
+ }
@@ -0,0 +1,14 @@
1
+ export type { FileStorageAdapter } from './interface'
2
+ export { createLocalStorageFileAdapter } from './localStorage'
3
+ export { createR2FileAdapter } from './r2'
4
+ export type { R2FileAdapterOptions } from './r2'
5
+ export { createS3FileAdapter } from './s3'
6
+ export type { S3FileAdapterOptions } from './s3'
7
+ export { createUploadthingFileAdapter } from './uploadthing'
8
+ export type { UploadthingFileAdapterOptions } from './uploadthing'
9
+ export { createSupabaseFileAdapter } from './supabase-storage'
10
+ export type { SupabaseFileAdapterOptions } from './supabase-storage'
11
+ export { createConvexFileAdapter } from './convex'
12
+ export type { ConvexFileAdapterOptions } from './convex'
13
+ export { createMinioFileAdapter } from './minio'
14
+ export type { MinioFileAdapterOptions } from './minio'
@@ -0,0 +1,11 @@
1
+ // @geenius/adapters — File Storage adapter interface
2
+
3
+ import type { StoredFile } from '../types'
4
+
5
+ export interface FileStorageAdapter {
6
+ upload(file: File | Blob, path: string, name?: string): Promise<StoredFile>
7
+ download(fileId: string): Promise<Blob>
8
+ delete(fileId: string): Promise<boolean>
9
+ list(prefix?: string): Promise<StoredFile[]>
10
+ getUrl(fileId: string): Promise<string>
11
+ }
@@ -0,0 +1,95 @@
1
+ // @geenius/adapters — localStorage File Storage implementation (base64 blobs)
2
+
3
+ import type { StoredFile } from '../types'
4
+ import type { FileStorageAdapter } from './interface'
5
+
6
+ const FILES_KEY = 'geenius_files'
7
+ const BLOBS_PREFIX = 'geenius_blob_'
8
+
9
+ interface StoredFileEntry extends StoredFile {
10
+ blobKey: string
11
+ }
12
+
13
+ function getFiles(): StoredFileEntry[] {
14
+ try { return JSON.parse(localStorage.getItem(FILES_KEY) || '[]') } catch { return [] }
15
+ }
16
+ function saveFiles(files: StoredFileEntry[]) { localStorage.setItem(FILES_KEY, JSON.stringify(files)) }
17
+
18
+ function blobToBase64(blob: Blob): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ const reader = new FileReader()
21
+ reader.onload = () => resolve(reader.result as string)
22
+ reader.onerror = reject
23
+ reader.readAsDataURL(blob)
24
+ })
25
+ }
26
+
27
+ function base64ToBlob(dataUrl: string): Blob {
28
+ const [meta, data] = dataUrl.split(',')
29
+ const mime = meta.match(/:(.*?);/)?.[1] || 'application/octet-stream'
30
+ const bytes = atob(data)
31
+ const buffer = new Uint8Array(bytes.length)
32
+ for (let i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i)
33
+ return new Blob([buffer], { type: mime })
34
+ }
35
+
36
+ export function createLocalStorageFileAdapter(): FileStorageAdapter {
37
+ return {
38
+ async upload(file, path, name?) {
39
+ const base64 = await blobToBase64(file)
40
+ const id = crypto.randomUUID()
41
+ const blobKey = BLOBS_PREFIX + id
42
+
43
+ // Store the blob data separately (can be large)
44
+ localStorage.setItem(blobKey, base64)
45
+
46
+ const entry: StoredFileEntry = {
47
+ id,
48
+ name: name || (file instanceof File ? file.name : `file-${id.slice(0, 8)}`),
49
+ path,
50
+ size: file.size,
51
+ mimeType: file.type || 'application/octet-stream',
52
+ url: `local://${id}`,
53
+ createdAt: new Date().toISOString(),
54
+ blobKey,
55
+ }
56
+ const files = getFiles()
57
+ files.push(entry)
58
+ saveFiles(files)
59
+
60
+ const { blobKey: _, ...storedFile } = entry
61
+ return storedFile
62
+ },
63
+
64
+ async download(fileId) {
65
+ const entry = getFiles().find(f => f.id === fileId)
66
+ if (!entry) throw new Error(`File not found: ${fileId}`)
67
+ const base64 = localStorage.getItem(entry.blobKey)
68
+ if (!base64) throw new Error(`Blob data missing for file: ${fileId}`)
69
+ return base64ToBlob(base64)
70
+ },
71
+
72
+ async delete(fileId) {
73
+ const files = getFiles()
74
+ const idx = files.findIndex(f => f.id === fileId)
75
+ if (idx === -1) return false
76
+ localStorage.removeItem(files[idx].blobKey)
77
+ files.splice(idx, 1)
78
+ saveFiles(files)
79
+ return true
80
+ },
81
+
82
+ async list(prefix?) {
83
+ let files = getFiles()
84
+ if (prefix) files = files.filter(f => f.path.startsWith(prefix))
85
+ return files.map(({ blobKey: _, ...f }) => f)
86
+ },
87
+
88
+ async getUrl(fileId) {
89
+ const entry = getFiles().find(f => f.id === fileId)
90
+ if (!entry) throw new Error(`File not found: ${fileId}`)
91
+ const base64 = localStorage.getItem(entry.blobKey)
92
+ return base64 || entry.url
93
+ },
94
+ }
95
+ }
@@ -0,0 +1,47 @@
1
+ // @geenius/adapters — Minio file storage adapter (S3-compatible, self-hosted)
2
+ // Requires: @aws-sdk/client-s3, @aws-sdk/s3-request-presigner
3
+ // Minio is 100% S3-compatible — this is a convenience wrapper around the S3 adapter
4
+ // with Minio-specific defaults.
5
+
6
+ import type { FileStorageAdapter } from './interface'
7
+ import { createS3FileAdapter } from './s3'
8
+ import type { S3FileAdapterOptions } from './s3'
9
+
10
+ export interface MinioFileAdapterOptions {
11
+ /** Minio endpoint URL (e.g. https://minio.example.com or http://localhost:9000) */
12
+ endpoint: string
13
+ /** Minio access key */
14
+ accessKeyId: string
15
+ /** Minio secret key */
16
+ secretAccessKey: string
17
+ /** Bucket name */
18
+ bucket: string
19
+ /** Use path-style URLs (default: true — required for Minio) */
20
+ forcePathStyle?: boolean
21
+ /** Custom public URL prefix */
22
+ publicUrl?: string
23
+ /** Presigned URL expiry in seconds (default: 3600) */
24
+ presignedExpiry?: number
25
+ }
26
+
27
+ /**
28
+ * Minio file storage adapter — self-hosted S3-compatible object storage.
29
+ * Wraps the S3 adapter with Minio-specific defaults.
30
+ * https://min.io
31
+ */
32
+ export function createMinioFileAdapter(options: MinioFileAdapterOptions): FileStorageAdapter {
33
+ const { endpoint, accessKeyId, secretAccessKey, bucket, publicUrl, presignedExpiry } = options
34
+
35
+ // Minio uses the same S3 API — delegate to S3 adapter with custom endpoint
36
+ const s3Options: S3FileAdapterOptions = {
37
+ region: 'us-east-1', // Minio ignores region but S3 client requires it
38
+ bucket,
39
+ accessKeyId,
40
+ secretAccessKey,
41
+ endpoint,
42
+ publicUrl: publicUrl || `${endpoint}/${bucket}`,
43
+ presignedExpiry,
44
+ }
45
+
46
+ return createS3FileAdapter(s3Options)
47
+ }
@@ -0,0 +1,123 @@
1
+ // @geenius/adapters — Cloudflare R2 file storage adapter
2
+ // Uses S3-compatible API via @aws-sdk/client-s3
3
+ // Requires: @aws-sdk/client-s3, @aws-sdk/s3-request-presigner
4
+
5
+ import type { StoredFile } from '../types'
6
+ import type { FileStorageAdapter } from './interface'
7
+
8
+ export interface R2FileAdapterOptions {
9
+ /** R2 account ID */
10
+ accountId: string
11
+ /** R2 access key ID */
12
+ accessKeyId: string
13
+ /** R2 secret access key */
14
+ secretAccessKey: string
15
+ /** R2 bucket name */
16
+ bucket: string
17
+ /** Custom public URL prefix (e.g. https://files.example.com) */
18
+ publicUrl?: string
19
+ /** Presigned URL expiry in seconds (default: 3600) */
20
+ presignedExpiry?: number
21
+ }
22
+
23
+ export function createR2FileAdapter(options: R2FileAdapterOptions): FileStorageAdapter {
24
+ const { bucket, accountId, accessKeyId, secretAccessKey, publicUrl, presignedExpiry = 3600 } = options
25
+
26
+ let s3Client: any = null
27
+ let getSignedUrl: any = null
28
+ let commands: any = null
29
+
30
+ async function getClient() {
31
+ if (!s3Client) {
32
+ const { S3Client } = await import('@aws-sdk/client-s3')
33
+ const presigner = await import('@aws-sdk/s3-request-presigner')
34
+ const s3Commands = await import('@aws-sdk/client-s3')
35
+
36
+ s3Client = new S3Client({
37
+ region: 'auto',
38
+ endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
39
+ credentials: { accessKeyId, secretAccessKey },
40
+ })
41
+ getSignedUrl = presigner.getSignedUrl
42
+ commands = s3Commands
43
+ }
44
+ return { client: s3Client, getSignedUrl, commands }
45
+ }
46
+
47
+ return {
48
+ async upload(file, path, name?) {
49
+ const { client, commands: cmd } = await getClient()
50
+ const id = crypto.randomUUID()
51
+ const fileName = name || (file instanceof File ? file.name : `file-${id.slice(0, 8)}`)
52
+ const key = path ? `${path}/${fileName}` : fileName
53
+ const buffer = new Uint8Array(await file.arrayBuffer())
54
+
55
+ await client.send(new cmd.PutObjectCommand({
56
+ Bucket: bucket,
57
+ Key: key,
58
+ Body: buffer,
59
+ ContentType: file.type || 'application/octet-stream',
60
+ Metadata: { 'original-name': fileName, 'file-id': id },
61
+ }))
62
+
63
+ const url = publicUrl ? `${publicUrl}/${key}` : `https://${accountId}.r2.cloudflarestorage.com/${bucket}/${key}`
64
+
65
+ return {
66
+ id,
67
+ name: fileName,
68
+ path: key,
69
+ size: file.size,
70
+ mimeType: file.type || 'application/octet-stream',
71
+ url,
72
+ createdAt: new Date().toISOString(),
73
+ }
74
+ },
75
+
76
+ async download(fileId) {
77
+ const { client, commands: cmd } = await getClient()
78
+ // fileId is used as the key/path
79
+ const response = await client.send(new cmd.GetObjectCommand({
80
+ Bucket: bucket,
81
+ Key: fileId,
82
+ }))
83
+ const bytes = await response.Body?.transformToByteArray()
84
+ if (!bytes) throw new Error(`Failed to download file: ${fileId}`)
85
+ return new Blob([bytes], { type: response.ContentType || 'application/octet-stream' })
86
+ },
87
+
88
+ async delete(fileId) {
89
+ const { client, commands: cmd } = await getClient()
90
+ try {
91
+ await client.send(new cmd.DeleteObjectCommand({ Bucket: bucket, Key: fileId }))
92
+ return true
93
+ } catch {
94
+ return false
95
+ }
96
+ },
97
+
98
+ async list(prefix?) {
99
+ const { client, commands: cmd } = await getClient()
100
+ const response = await client.send(new cmd.ListObjectsV2Command({
101
+ Bucket: bucket,
102
+ Prefix: prefix || undefined,
103
+ }))
104
+
105
+ return (response.Contents || []).map((obj: any): StoredFile => ({
106
+ id: obj.Key,
107
+ name: obj.Key.split('/').pop() || obj.Key,
108
+ path: obj.Key,
109
+ size: obj.Size || 0,
110
+ mimeType: 'application/octet-stream',
111
+ url: publicUrl ? `${publicUrl}/${obj.Key}` : `https://${accountId}.r2.cloudflarestorage.com/${bucket}/${obj.Key}`,
112
+ createdAt: obj.LastModified?.toISOString() || new Date().toISOString(),
113
+ }))
114
+ },
115
+
116
+ async getUrl(fileId) {
117
+ if (publicUrl) return `${publicUrl}/${fileId}`
118
+ const { client, getSignedUrl: sign, commands: cmd } = await getClient()
119
+ const command = new cmd.GetObjectCommand({ Bucket: bucket, Key: fileId })
120
+ return sign(client, command, { expiresIn: presignedExpiry })
121
+ },
122
+ }
123
+ }