@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,545 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import {
3
+ // Tier gate
4
+ isFeatureAvailable, isPackageAvailable,
5
+ getAvailableFeatures, getAvailablePackages,
6
+ getUpgradeFeatures, DEFAULT_TIER_GATE,
7
+ // Config
8
+ configureAdapters, getAdapterConfig, isAdaptersConfigured, resetAdapterConfig, getDomainConfig,
9
+ // Constants
10
+ ADAPTER_DOMAINS, DB_PROVIDERS, AUTH_PROVIDERS, AI_PROVIDERS,
11
+ STORAGE_PROVIDERS, PAYMENT_PROVIDERS, ADMIN_PROVIDERS,
12
+ PROVIDER_REGISTRY, getProvidersForDomain, getProviderMeta,
13
+ DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS,
14
+ // Factories
15
+ createLocalStorageAuthAdapter,
16
+ createLocalStorageDbAdapter,
17
+ createLocalStoragePaymentsAdapter,
18
+ createNoopPaymentsAdapter,
19
+ createLocalStorageAiAdapter,
20
+ createLocalStorageFileAdapter,
21
+ createLocalStorageAdminAdapter,
22
+ } from '../index'
23
+
24
+ // ─── localStorage mock ───────────────────────────────────────────────────────
25
+ // Vitest runs in Node (jsdom or node env). We need a minimal localStorage mock.
26
+
27
+ const localStorageStore: Record<string, string> = {}
28
+
29
+ const localStorageMock = {
30
+ getItem: (key: string) => localStorageStore[key] ?? null,
31
+ setItem: (key: string, value: string) => { localStorageStore[key] = value },
32
+ removeItem: (key: string) => { delete localStorageStore[key] },
33
+ clear: () => { Object.keys(localStorageStore).forEach(k => delete localStorageStore[k]) },
34
+ get length() { return Object.keys(localStorageStore).length },
35
+ key: (index: number) => Object.keys(localStorageStore)[index] ?? null,
36
+ }
37
+
38
+ vi.stubGlobal('localStorage', localStorageMock)
39
+ vi.stubGlobal('crypto', {
40
+ randomUUID: () => `${Math.random().toString(36).slice(2)}-${Date.now()}`,
41
+ })
42
+
43
+ function clearStorage() {
44
+ localStorageMock.clear()
45
+ }
46
+
47
+ // ─── Tier Gate ───────────────────────────────────────────────────────────────
48
+
49
+ describe('Tier Gate — isFeatureAvailable', () => {
50
+ it('pronto tier has basic features', () => {
51
+ expect(isFeatureAvailable('auth', 'pronto')).toBe(true)
52
+ })
53
+
54
+ it('pronto tier lacks premium features', () => {
55
+ expect(isFeatureAvailable('ai-magic', 'pronto')).toBe(false)
56
+ })
57
+
58
+ it('lancio tier has intermediate features', () => {
59
+ expect(isFeatureAvailable('admin', 'lancio')).toBe(true)
60
+ expect(isFeatureAvailable('payments', 'lancio')).toBe(true)
61
+ })
62
+
63
+ it('lancio tier lacks studio-only features', () => {
64
+ expect(isFeatureAvailable('ai-magic', 'lancio')).toBe(false)
65
+ })
66
+
67
+ it('studio tier has all features', () => {
68
+ expect(isFeatureAvailable('ai-magic', 'studio')).toBe(true)
69
+ expect(isFeatureAvailable('analytics', 'studio')).toBe(true)
70
+ expect(isFeatureAvailable('monitoring', 'studio')).toBe(true)
71
+ })
72
+
73
+ it('returns false for unknown features', () => {
74
+ expect(isFeatureAvailable('nonexistent-feature', 'studio')).toBe(false)
75
+ })
76
+ })
77
+
78
+ describe('Tier Gate — isPackageAvailable', () => {
79
+ it('returns true for packages in tier', () => {
80
+ expect(isPackageAvailable('@geenius/auth', 'lancio')).toBe(true)
81
+ expect(isPackageAvailable('@geenius/auth', 'pronto')).toBe(true)
82
+ expect(isPackageAvailable('@geenius/auth', 'studio')).toBe(true)
83
+ })
84
+
85
+ it('returns false for unavailable packages', () => {
86
+ expect(isPackageAvailable('@geenius/admin', 'pronto')).toBe(false)
87
+ expect(isPackageAvailable('@geenius/ai-magic', 'lancio')).toBe(false)
88
+ })
89
+
90
+ it('studio has all packages', () => {
91
+ expect(isPackageAvailable('@geenius/agent', 'studio')).toBe(true)
92
+ })
93
+ })
94
+
95
+ describe('Tier Gate — getAvailable and getUpgrade', () => {
96
+ it('getAvailableFeatures returns array', () => {
97
+ const features = getAvailableFeatures('lancio')
98
+ expect(Array.isArray(features)).toBe(true)
99
+ expect(features.length).toBeGreaterThan(0)
100
+ })
101
+
102
+ it('getAvailablePackages returns array', () => {
103
+ const pkgs = getAvailablePackages('studio')
104
+ expect(Array.isArray(pkgs)).toBe(true)
105
+ expect(pkgs.length).toBeGreaterThan(0)
106
+ })
107
+
108
+ it('getUpgradeFeatures shows what pronto is missing vs lancio', () => {
109
+ const upgrades = getUpgradeFeatures('pronto', 'lancio')
110
+ expect(Array.isArray(upgrades)).toBe(true)
111
+ expect(upgrades).toContain('admin')
112
+ expect(upgrades).toContain('payments')
113
+ expect(upgrades).not.toContain('auth') // already in pronto
114
+ })
115
+
116
+ it('getUpgradeFeatures shows what lancio is missing vs studio', () => {
117
+ const upgrades = getUpgradeFeatures('lancio', 'studio')
118
+ expect(Array.isArray(upgrades)).toBe(true)
119
+ expect(upgrades).toContain('ai-magic')
120
+ expect(upgrades).toContain('analytics')
121
+ expect(upgrades).not.toContain('admin') // already in lancio
122
+ })
123
+
124
+ it('getUpgradeFeatures returns empty when already at same tier', () => {
125
+ const upgrades = getUpgradeFeatures('studio', 'studio')
126
+ expect(upgrades).toHaveLength(0)
127
+ })
128
+
129
+ it('DEFAULT_TIER_GATE is defined and has three tiers', () => {
130
+ expect(DEFAULT_TIER_GATE).toBeDefined()
131
+ expect(typeof DEFAULT_TIER_GATE).toBe('object')
132
+ expect(DEFAULT_TIER_GATE.features).toHaveProperty('pronto')
133
+ expect(DEFAULT_TIER_GATE.features).toHaveProperty('lancio')
134
+ expect(DEFAULT_TIER_GATE.features).toHaveProperty('studio')
135
+ expect(DEFAULT_TIER_GATE.packages).toHaveProperty('pronto')
136
+ expect(DEFAULT_TIER_GATE.packages).toHaveProperty('lancio')
137
+ expect(DEFAULT_TIER_GATE.packages).toHaveProperty('studio')
138
+ })
139
+ })
140
+
141
+ // ─── Configuration ────────────────────────────────────────────────────────────
142
+
143
+ describe('configureAdapters', () => {
144
+ beforeEach(() => {
145
+ resetAdapterConfig()
146
+ })
147
+
148
+ it('isAdaptersConfigured returns false before configure()', () => {
149
+ expect(isAdaptersConfigured()).toBe(false)
150
+ })
151
+
152
+ it('configureAdapters sets config and isAdaptersConfigured returns true', () => {
153
+ configureAdapters({ db: { provider: 'convex' } })
154
+ expect(isAdaptersConfigured()).toBe(true)
155
+ })
156
+
157
+ it('getAdapterConfig returns the config', () => {
158
+ configureAdapters({ db: { provider: 'convex' }, auth: { provider: 'clerk' } })
159
+ const cfg = getAdapterConfig()
160
+ expect(cfg.db?.provider).toBe('convex')
161
+ expect(cfg.auth?.provider).toBe('clerk')
162
+ })
163
+
164
+ it('getAdapterConfig throws when not configured', () => {
165
+ expect(() => getAdapterConfig()).toThrow('Adapters not configured')
166
+ })
167
+
168
+ it('getDomainConfig returns specific domain config', () => {
169
+ configureAdapters({ ai: { provider: 'openai', apiKey: 'sk-test' } })
170
+ const ai = getDomainConfig('ai')
171
+ expect(ai?.provider).toBe('openai')
172
+ expect(ai?.apiKey).toBe('sk-test')
173
+ })
174
+
175
+ it('getDomainConfig returns null for unconfigured domains', () => {
176
+ configureAdapters({ db: { provider: 'convex' } })
177
+ expect(getDomainConfig('auth')).toBeNull()
178
+ })
179
+
180
+ it('resetAdapterConfig allows reconfiguring', () => {
181
+ configureAdapters({ db: { provider: 'convex' } })
182
+ resetAdapterConfig()
183
+ expect(isAdaptersConfigured()).toBe(false)
184
+ configureAdapters({ db: { provider: 'neon' } })
185
+ expect(getAdapterConfig().db?.provider).toBe('neon')
186
+ })
187
+ })
188
+
189
+ // ─── Constants ────────────────────────────────────────────────────────────────
190
+
191
+ describe('Constants & Registry', () => {
192
+ it('ADAPTER_DOMAINS has all 6 domains', () => {
193
+ expect(ADAPTER_DOMAINS).toHaveLength(6)
194
+ expect(ADAPTER_DOMAINS).toContain('db')
195
+ expect(ADAPTER_DOMAINS).toContain('auth')
196
+ expect(ADAPTER_DOMAINS).toContain('ai')
197
+ expect(ADAPTER_DOMAINS).toContain('storage')
198
+ expect(ADAPTER_DOMAINS).toContain('payments')
199
+ expect(ADAPTER_DOMAINS).toContain('admin')
200
+ })
201
+
202
+ it('provider arrays contain localStorage as first entry', () => {
203
+ expect(DB_PROVIDERS[0]).toBe('localStorage')
204
+ expect(AUTH_PROVIDERS[0]).toBe('localStorage')
205
+ expect(AI_PROVIDERS[0]).toBe('localStorage')
206
+ expect(STORAGE_PROVIDERS[0]).toBe('localStorage')
207
+ expect(PAYMENT_PROVIDERS[0]).toBe('localStorage')
208
+ expect(ADMIN_PROVIDERS[0]).toBe('localStorage')
209
+ })
210
+
211
+ it('DOMAIN_LABELS covers all domains', () => {
212
+ for (const domain of ADAPTER_DOMAINS) {
213
+ expect(DOMAIN_LABELS[domain]).toBeTruthy()
214
+ }
215
+ })
216
+
217
+ it('DOMAIN_ICONS covers all domains', () => {
218
+ for (const domain of ADAPTER_DOMAINS) {
219
+ expect(DOMAIN_ICONS[domain]).toBeTruthy()
220
+ }
221
+ })
222
+
223
+ it('DOMAIN_DESCRIPTIONS covers all domains', () => {
224
+ for (const domain of ADAPTER_DOMAINS) {
225
+ expect(DOMAIN_DESCRIPTIONS[domain]).toBeTruthy()
226
+ }
227
+ })
228
+
229
+ it('PROVIDER_REGISTRY has entries for all domains', () => {
230
+ for (const domain of ADAPTER_DOMAINS) {
231
+ const entries = PROVIDER_REGISTRY.filter(p => p.domain === domain)
232
+ expect(entries.length).toBeGreaterThan(0)
233
+ }
234
+ })
235
+
236
+ it('PROVIDER_REGISTRY uses correct tier names (pronto | lancio | studio)', () => {
237
+ const validTiers = new Set(['pronto', 'lancio', 'studio'])
238
+ for (const entry of PROVIDER_REGISTRY) {
239
+ expect(validTiers.has(entry.tier)).toBe(true)
240
+ }
241
+ })
242
+
243
+ it('getProvidersForDomain returns filtered list', () => {
244
+ const dbProviders = getProvidersForDomain('db')
245
+ expect(dbProviders.every(p => p.domain === 'db')).toBe(true)
246
+ expect(dbProviders.length).toBe(5) // localStorage, convex, supabase, neon, mongodb
247
+ })
248
+
249
+ it('getProviderMeta finds by domain + id', () => {
250
+ const stripe = getProviderMeta('payments', 'stripe')
251
+ expect(stripe).toBeDefined()
252
+ expect(stripe?.name).toBe('Stripe')
253
+ expect(stripe?.tier).toBe('lancio')
254
+ })
255
+
256
+ it('getProviderMeta returns undefined for unknown provider', () => {
257
+ expect(getProviderMeta('db', 'firebase')).toBeUndefined()
258
+ })
259
+ })
260
+
261
+ // ─── localStorage Auth Adapter ────────────────────────────────────────────────
262
+
263
+ describe('createLocalStorageAuthAdapter', () => {
264
+ beforeEach(() => clearStorage())
265
+
266
+ it('signUp creates a user and returns a session', async () => {
267
+ const auth = createLocalStorageAuthAdapter()
268
+ const session = await auth.signUp('test@example.com', 'password123', 'Test User')
269
+ expect(session.userId).toBeTruthy()
270
+ expect(session.token).toBeTruthy()
271
+ expect(session.expiresAt).toBeTruthy()
272
+ })
273
+
274
+ it('signUp throws if email already registered', async () => {
275
+ const auth = createLocalStorageAuthAdapter()
276
+ await auth.signUp('dup@example.com', 'pass')
277
+ await expect(auth.signUp('dup@example.com', 'pass')).rejects.toThrow('User already exists')
278
+ })
279
+
280
+ it('signIn returns session with correct credentials', async () => {
281
+ const auth = createLocalStorageAuthAdapter()
282
+ await auth.signUp('user@example.com', 'secret')
283
+ const session = await auth.signIn('user@example.com', 'secret')
284
+ expect(session.userId).toBeTruthy()
285
+ })
286
+
287
+ it('signIn throws on wrong password', async () => {
288
+ const auth = createLocalStorageAuthAdapter()
289
+ await auth.signUp('user@example.com', 'correct')
290
+ await expect(auth.signIn('user@example.com', 'wrong')).rejects.toThrow('Invalid email or password')
291
+ })
292
+
293
+ it('getSession returns current session', async () => {
294
+ const auth = createLocalStorageAuthAdapter()
295
+ await auth.signUp('user@example.com', 'pw')
296
+ const session = await auth.getSession()
297
+ expect(session).not.toBeNull()
298
+ })
299
+
300
+ it('signOut clears the session', async () => {
301
+ const auth = createLocalStorageAuthAdapter()
302
+ await auth.signUp('user@example.com', 'pw')
303
+ await auth.signOut()
304
+ const session = await auth.getSession()
305
+ expect(session).toBeNull()
306
+ })
307
+
308
+ it('getUser returns the current user', async () => {
309
+ const auth = createLocalStorageAuthAdapter()
310
+ await auth.signUp('user@example.com', 'pw', 'Alice')
311
+ const user = await auth.getUser()
312
+ expect(user?.email).toBe('user@example.com')
313
+ expect(user?.name).toBe('Alice')
314
+ expect((user as any)?.passwordHash).toBeUndefined() // never leak the hash
315
+ })
316
+
317
+ it('getUser returns null when not signed in', async () => {
318
+ const auth = createLocalStorageAuthAdapter()
319
+ const user = await auth.getUser()
320
+ expect(user).toBeNull()
321
+ })
322
+
323
+ it('updateUser mutates name and image', async () => {
324
+ const auth = createLocalStorageAuthAdapter()
325
+ await auth.signUp('user@example.com', 'pw', 'Original')
326
+ const updated = await auth.updateUser({ name: 'Updated', image: 'https://example.com/avatar.png' })
327
+ expect(updated?.name).toBe('Updated')
328
+ expect(updated?.image).toBe('https://example.com/avatar.png')
329
+ })
330
+
331
+ it('signInWithOAuth returns a redirect url', async () => {
332
+ const auth = createLocalStorageAuthAdapter()
333
+ const result = await auth.signInWithOAuth('google', { redirectUrl: '/dashboard' })
334
+ expect(result.url).toBe('/dashboard')
335
+ // Should also have created a session
336
+ const session = await auth.getSession()
337
+ expect(session).not.toBeNull()
338
+ })
339
+ })
340
+
341
+ // ─── localStorage DB Adapter ─────────────────────────────────────────────────
342
+
343
+ describe('createLocalStorageDbAdapter', () => {
344
+ beforeEach(() => clearStorage())
345
+
346
+ it('create and get round-trips', async () => {
347
+ const db = createLocalStorageDbAdapter()
348
+ const created = await db.create<{ id: string; name: string; value: number }>('items', { name: 'test', value: 42 })
349
+ expect(created.id).toBeTruthy()
350
+ expect(created.name).toBe('test')
351
+ const fetched = await db.get<typeof created>('items', created.id)
352
+ expect(fetched).toEqual(created)
353
+ })
354
+
355
+ it('get returns null for missing id', async () => {
356
+ const db = createLocalStorageDbAdapter()
357
+ const result = await db.get('items', 'nonexistent')
358
+ expect(result).toBeNull()
359
+ })
360
+
361
+ it('update modifies fields', async () => {
362
+ const db = createLocalStorageDbAdapter()
363
+ const item = await db.create<{ id: string; name: string }>('items', { name: 'original' })
364
+ const updated = await db.update<typeof item>('items', item.id, { name: 'changed' })
365
+ expect(updated.name).toBe('changed')
366
+ })
367
+
368
+ it('delete removes the item', async () => {
369
+ const db = createLocalStorageDbAdapter()
370
+ const item = await db.create<{ id: string; val: string }>('things', { val: 'x' })
371
+ await db.delete('things', item.id)
372
+ const result = await db.get('things', item.id)
373
+ expect(result).toBeNull()
374
+ })
375
+
376
+ it('list returns all items in a table', async () => {
377
+ const db = createLocalStorageDbAdapter()
378
+ await db.create('docs', { title: 'A' })
379
+ await db.create('docs', { title: 'B' })
380
+ const all = await db.list('docs')
381
+ expect(all.length).toBe(2)
382
+ })
383
+
384
+ it('query filters by field equality', async () => {
385
+ const db = createLocalStorageDbAdapter()
386
+ await db.create('users', { role: 'admin' })
387
+ await db.create('users', { role: 'user' })
388
+ await db.create('users', { role: 'admin' })
389
+ const admins = await db.query('users', [{ field: 'role', operator: 'eq', value: 'admin' }])
390
+ expect(admins.length).toBe(2)
391
+ })
392
+ })
393
+
394
+ // ─── localStorage Payments Adapter ───────────────────────────────────────────
395
+
396
+ describe('createLocalStoragePaymentsAdapter', () => {
397
+ beforeEach(() => clearStorage())
398
+
399
+ it('getPlans returns default plans', async () => {
400
+ const payments = createLocalStoragePaymentsAdapter()
401
+ const plans = await payments.getPlans()
402
+ expect(plans.length).toBeGreaterThan(0)
403
+ expect(plans[0]).toHaveProperty('id')
404
+ expect(plans[0]).toHaveProperty('price')
405
+ })
406
+
407
+ it('createCheckout creates a subscription', async () => {
408
+ const payments = createLocalStoragePaymentsAdapter()
409
+ const result = await payments.createCheckout({ planId: 'pro', userId: 'user-1', successUrl: '/success' })
410
+ expect(result.sessionId).toBeTruthy()
411
+ expect(result.url).toBe('/success')
412
+ })
413
+
414
+ it('getSubscription returns active subscription', async () => {
415
+ const payments = createLocalStoragePaymentsAdapter()
416
+ await payments.createCheckout({ planId: 'pro', userId: 'user-1' })
417
+ const sub = await payments.getSubscription('user-1')
418
+ expect(sub).not.toBeNull()
419
+ expect(sub?.status).toBe('active')
420
+ expect(sub?.planId).toBe('pro')
421
+ })
422
+
423
+ it('cancelSubscription marks subscription cancelled', async () => {
424
+ const payments = createLocalStoragePaymentsAdapter()
425
+ const { sessionId } = await payments.createCheckout({ planId: 'pro', userId: 'user-2' })
426
+ await payments.cancelSubscription(sessionId)
427
+ const sub = await payments.getSubscription('user-2')
428
+ expect(sub?.status).toBe('cancelled')
429
+ })
430
+
431
+ it('isFeatureEnabled returns false for free plan (no AI features)', async () => {
432
+ const payments = createLocalStoragePaymentsAdapter()
433
+ await payments.createCheckout({ planId: 'free', userId: 'user-3' })
434
+ const enabled = await payments.isFeatureEnabled('user-3', 'AI features')
435
+ expect(enabled).toBe(false)
436
+ })
437
+
438
+ it('isFeatureEnabled returns true for pro plan with matching feature', async () => {
439
+ const payments = createLocalStoragePaymentsAdapter()
440
+ await payments.createCheckout({ planId: 'pro', userId: 'user-4' })
441
+ const enabled = await payments.isFeatureEnabled('user-4', 'AI features')
442
+ expect(enabled).toBe(true)
443
+ })
444
+ })
445
+
446
+ describe('createNoopPaymentsAdapter', () => {
447
+ it('createCheckout throws with Lancio upgrade message', async () => {
448
+ const noop = createNoopPaymentsAdapter()
449
+ await expect(noop.createCheckout({ planId: 'pro', userId: 'u1' })).rejects.toThrow('Lancio')
450
+ })
451
+
452
+ it('getSubscription returns null', async () => {
453
+ const noop = createNoopPaymentsAdapter()
454
+ expect(await noop.getSubscription('u1')).toBeNull()
455
+ })
456
+
457
+ it('isFeatureEnabled returns false', async () => {
458
+ const noop = createNoopPaymentsAdapter()
459
+ expect(await noop.isFeatureEnabled('u1', 'any-feature')).toBe(false)
460
+ })
461
+ })
462
+
463
+ // ─── localStorage AI Adapter ─────────────────────────────────────────────────
464
+
465
+ describe('createLocalStorageAiAdapter', () => {
466
+ it('chat returns a deterministic response', async () => {
467
+ const ai = createLocalStorageAiAdapter()
468
+ const response = await ai.chat([{ role: 'user', content: 'Hello' }])
469
+ expect(response.content).toBeTruthy()
470
+ expect(response.finishReason).toBe('stop')
471
+ expect(response.usage).toBeDefined()
472
+ })
473
+
474
+ it('complete returns a string', async () => {
475
+ const ai = createLocalStorageAiAdapter()
476
+ const result = await ai.complete('What is 2+2?')
477
+ expect(typeof result).toBe('string')
478
+ expect(result.length).toBeGreaterThan(0)
479
+ })
480
+
481
+ it('embed returns array of embeddings', async () => {
482
+ const ai = createLocalStorageAiAdapter()
483
+ const embeddings = await ai.embed(['hello', 'world'])
484
+ expect(Array.isArray(embeddings)).toBe(true)
485
+ expect(embeddings.length).toBe(2)
486
+ expect(Array.isArray(embeddings[0])).toBe(true)
487
+ })
488
+ })
489
+
490
+ // ─── localStorage File Adapter ────────────────────────────────────────────────
491
+
492
+ describe('createLocalStorageFileAdapter', () => {
493
+ beforeEach(() => clearStorage())
494
+
495
+ it('upload returns a StoredFile', async () => {
496
+ const storage = createLocalStorageFileAdapter()
497
+ const mockFile = new Blob(['hello'], { type: 'text/plain' })
498
+ const file = await storage.upload(mockFile as File, 'hello.txt')
499
+ expect(file.id).toBeTruthy()
500
+ expect(file.name).toBe('hello.txt')
501
+ })
502
+
503
+ it('list returns uploaded files', async () => {
504
+ const storage = createLocalStorageFileAdapter()
505
+ await storage.upload(new Blob(['a']) as File, 'a.txt')
506
+ await storage.upload(new Blob(['b']) as File, 'b.txt')
507
+ const files = await storage.list()
508
+ expect(files.length).toBe(2)
509
+ })
510
+
511
+ it('delete removes a file', async () => {
512
+ const storage = createLocalStorageFileAdapter()
513
+ const file = await storage.upload(new Blob(['data']) as File, 'to-delete.txt')
514
+ await storage.delete(file.id)
515
+ const files = await storage.list()
516
+ expect(files.find(f => f.id === file.id)).toBeUndefined()
517
+ })
518
+
519
+ it('getUrl returns a url string', async () => {
520
+ const storage = createLocalStorageFileAdapter()
521
+ const file = await storage.upload(new Blob(['x']) as File, 'test.txt')
522
+ const url = await storage.getUrl(file.id)
523
+ expect(typeof url).toBe('string')
524
+ })
525
+ })
526
+
527
+ // ─── localStorage Admin Adapter ───────────────────────────────────────────────
528
+
529
+ describe('createLocalStorageAdminAdapter', () => {
530
+ beforeEach(() => clearStorage())
531
+
532
+ it('getMetrics returns shaped metrics object', async () => {
533
+ const admin = createLocalStorageAdminAdapter()
534
+ const metrics = await admin.getMetrics()
535
+ expect(metrics).toHaveProperty('totalUsers')
536
+ expect(metrics).toHaveProperty('activeUsers')
537
+ expect(metrics).toHaveProperty('revenue')
538
+ })
539
+
540
+ it('listUsers returns an array', async () => {
541
+ const admin = createLocalStorageAdminAdapter()
542
+ const users = await admin.listUsers()
543
+ expect(Array.isArray(users)).toBe(true)
544
+ })
545
+ })
@@ -0,0 +1,2 @@
1
+ export type { AdminAdapter, AdminMetrics, ManagedUser } from './interface'
2
+ export { createLocalStorageAdminAdapter } from './localStorage'
@@ -0,0 +1,34 @@
1
+ // @geenius/adapters — Admin adapter interface
2
+ // Provides user management and dashboard metrics for admin panels.
3
+
4
+ import type { AuthUser } from '../types'
5
+
6
+ export interface AdminMetrics {
7
+ totalUsers: number
8
+ activeUsers: number
9
+ newUsersToday: number
10
+ totalRevenue?: number
11
+ mrr?: number
12
+ }
13
+
14
+ export interface ManagedUser extends AuthUser {
15
+ isActive: boolean
16
+ isBanned: boolean
17
+ plan?: string
18
+ lastLoginAt?: string
19
+ }
20
+
21
+ export interface AdminAdapter {
22
+ /** List all users with pagination */
23
+ listUsers(options?: { limit?: number; offset?: number; search?: string }): Promise<ManagedUser[]>
24
+ /** Get a single user by ID */
25
+ getUser(id: string): Promise<ManagedUser | null>
26
+ /** Ban a user */
27
+ banUser(id: string): Promise<boolean>
28
+ /** Unban a user */
29
+ unbanUser(id: string): Promise<boolean>
30
+ /** Delete a user */
31
+ deleteUser(id: string): Promise<boolean>
32
+ /** Get dashboard metrics */
33
+ getMetrics(): Promise<AdminMetrics>
34
+ }
@@ -0,0 +1,109 @@
1
+ // @geenius/adapters — localStorage admin implementation (Pronto/MVP tier)
2
+
3
+ import type { AdminAdapter, AdminMetrics, ManagedUser } from './interface'
4
+
5
+ const USERS_KEY = 'geenius_auth_users'
6
+
7
+ export function createLocalStorageAdminAdapter(): AdminAdapter {
8
+ function getUsers(): ManagedUser[] {
9
+ try {
10
+ const data = localStorage.getItem(USERS_KEY)
11
+ if (!data) return []
12
+ const raw = JSON.parse(data) as Record<string, any>
13
+ return Object.values(raw).map((u: any) => ({
14
+ id: u.id,
15
+ email: u.email,
16
+ name: u.name,
17
+ image: u.image,
18
+ role: u.role || 'user',
19
+ isActive: true,
20
+ isBanned: u.isBanned || false,
21
+ plan: u.plan || 'free',
22
+ createdAt: u.createdAt,
23
+ lastLoginAt: u.lastLoginAt,
24
+ }))
25
+ } catch {
26
+ return []
27
+ }
28
+ }
29
+
30
+ return {
31
+ async listUsers(options?) {
32
+ let users = getUsers()
33
+
34
+ if (options?.search) {
35
+ const q = options.search.toLowerCase()
36
+ users = users.filter(
37
+ (u) =>
38
+ u.email?.toLowerCase().includes(q) ||
39
+ u.name?.toLowerCase().includes(q)
40
+ )
41
+ }
42
+
43
+ const offset = options?.offset || 0
44
+ const limit = options?.limit || 50
45
+ return users.slice(offset, offset + limit)
46
+ },
47
+
48
+ async getUser(id) {
49
+ const users = getUsers()
50
+ return users.find((u) => u.id === id) || null
51
+ },
52
+
53
+ async banUser(id) {
54
+ try {
55
+ const data = JSON.parse(localStorage.getItem(USERS_KEY) || '{}')
56
+ if (data[id]) {
57
+ data[id].isBanned = true
58
+ localStorage.setItem(USERS_KEY, JSON.stringify(data))
59
+ return true
60
+ }
61
+ return false
62
+ } catch {
63
+ return false
64
+ }
65
+ },
66
+
67
+ async unbanUser(id) {
68
+ try {
69
+ const data = JSON.parse(localStorage.getItem(USERS_KEY) || '{}')
70
+ if (data[id]) {
71
+ data[id].isBanned = false
72
+ localStorage.setItem(USERS_KEY, JSON.stringify(data))
73
+ return true
74
+ }
75
+ return false
76
+ } catch {
77
+ return false
78
+ }
79
+ },
80
+
81
+ async deleteUser(id) {
82
+ try {
83
+ const data = JSON.parse(localStorage.getItem(USERS_KEY) || '{}')
84
+ if (data[id]) {
85
+ delete data[id]
86
+ localStorage.setItem(USERS_KEY, JSON.stringify(data))
87
+ return true
88
+ }
89
+ return false
90
+ } catch {
91
+ return false
92
+ }
93
+ },
94
+
95
+ async getMetrics(): Promise<AdminMetrics> {
96
+ const users = getUsers()
97
+ const today = new Date().toISOString().split('T')[0]
98
+ const newToday = users.filter(
99
+ (u) => u.createdAt && u.createdAt.startsWith(today)
100
+ ).length
101
+
102
+ return {
103
+ totalUsers: users.length,
104
+ activeUsers: users.filter((u) => u.isActive && !u.isBanned).length,
105
+ newUsersToday: newToday,
106
+ }
107
+ },
108
+ }
109
+ }