@cloudauth/adapter-drizzle 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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapter 2.js +181 -0
  3. package/dist/adapter.d 2.ts +7 -0
  4. package/dist/adapter.d.ts +7 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +181 -0
  7. package/dist/adapter.js 2.map +1 -0
  8. package/dist/adapter.js.map +1 -0
  9. package/dist/index 2.js +4 -0
  10. package/dist/index.d 2.ts +4 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts 2.map +1 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +4 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/schema-pg.d 2.ts +624 -0
  17. package/dist/schema-pg.d.ts +624 -0
  18. package/dist/schema-pg.d.ts 2.map +1 -0
  19. package/dist/schema-pg.d.ts.map +1 -0
  20. package/dist/schema-pg.js +52 -0
  21. package/dist/schema-pg.js 2.map +1 -0
  22. package/dist/schema-pg.js.map +1 -0
  23. package/dist/schema.d 2.ts +672 -0
  24. package/dist/schema.d.ts +672 -0
  25. package/dist/schema.d.ts 2.map +1 -0
  26. package/dist/schema.d.ts.map +1 -0
  27. package/dist/schema.js +52 -0
  28. package/dist/schema.js.map +1 -0
  29. package/dist/test/adapter.test 2.js +88 -0
  30. package/dist/test/adapter.test.d 2.ts +2 -0
  31. package/dist/test/adapter.test.d 3.ts +2 -0
  32. package/dist/test/adapter.test.d.ts +2 -0
  33. package/dist/test/adapter.test.d.ts 2.map +1 -0
  34. package/dist/test/adapter.test.d.ts 3.map +1 -0
  35. package/dist/test/adapter.test.d.ts.map +1 -0
  36. package/dist/test/adapter.test.js +88 -0
  37. package/dist/test/adapter.test.js 2.map +1 -0
  38. package/dist/test/adapter.test.js 3.map +1 -0
  39. package/dist/test/adapter.test.js.map +1 -0
  40. package/dist/test/auth.test.d 2.ts +2 -0
  41. package/dist/test/auth.test.d.ts +2 -0
  42. package/dist/test/auth.test.d.ts 2.map +1 -0
  43. package/dist/test/auth.test.d.ts.map +1 -0
  44. package/dist/test/auth.test.js +181 -0
  45. package/dist/test/auth.test.js 2.map +1 -0
  46. package/dist/test/auth.test.js.map +1 -0
  47. package/package.json +36 -0
  48. package/src/adapter.ts +227 -0
  49. package/src/index.ts +3 -0
  50. package/src/schema-pg.ts +62 -0
  51. package/src/schema.ts +62 -0
  52. package/src/test/adapter.test.ts +93 -0
  53. package/src/test/auth.test.ts +211 -0
  54. package/tsconfig 2.tsbuildinfo +1 -0
  55. package/tsconfig 3.tsbuildinfo +1 -0
  56. package/tsconfig.json +11 -0
  57. package/tsconfig.tsbuildinfo +1 -0
  58. package/vitest.config.ts +7 -0
package/src/adapter.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { eq, sql, and } from 'drizzle-orm'
2
+ import type { AuthAdapter } from '@cloudauth/core'
3
+ import type {
4
+ CreateUserInput,
5
+ UpdateUserInput,
6
+ CreateAccountInput,
7
+ CreateSessionInput,
8
+ UpdateSessionInput,
9
+ CreateOAuthStateInput,
10
+ User,
11
+ Account,
12
+ Session,
13
+ OAuthState,
14
+ SessionWithUser,
15
+ } from '@cloudauth/core'
16
+ import * as schema from './schema.js'
17
+ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
18
+
19
+ type Database = LibSQLDatabase<typeof schema>
20
+
21
+ function now(): Date {
22
+ return new Date()
23
+ }
24
+
25
+ function assertRow<T>(row: T | undefined): T {
26
+ if (!row) {
27
+ throw new Error('Expected a row to be returned from the database')
28
+ }
29
+ return row
30
+ }
31
+
32
+ export function drizzleAdapter(db: Database): AuthAdapter {
33
+ const adapter: AuthAdapter = {
34
+ // --- User operations ---
35
+
36
+ async createUser(data: CreateUserInput): Promise<User> {
37
+ const [row] = await db
38
+ .insert(schema.users)
39
+ .values({
40
+ id: crypto.randomUUID(),
41
+ email: data.email,
42
+ emailVerified: data.emailVerified,
43
+ name: data.name,
44
+ image: data.image,
45
+ createdAt: now(),
46
+ updatedAt: now(),
47
+ })
48
+ .returning()
49
+ return assertRow(row)
50
+ },
51
+
52
+ async getUserById(id: string): Promise<User | null> {
53
+ const [row] = await db.select().from(schema.users).where(eq(schema.users.id, id))
54
+ return row ?? null
55
+ },
56
+
57
+ async getUserByEmail(email: string): Promise<User | null> {
58
+ if (!email) return null
59
+ const [row] = await db.select().from(schema.users).where(eq(schema.users.email, email))
60
+ return row ?? null
61
+ },
62
+
63
+ async updateUser(id: string, data: UpdateUserInput): Promise<User> {
64
+ const [row] = await db
65
+ .update(schema.users)
66
+ .set({
67
+ ...data,
68
+ updatedAt: now(),
69
+ })
70
+ .where(eq(schema.users.id, id))
71
+ .returning()
72
+ return assertRow(row)
73
+ },
74
+
75
+ // --- Account operations ---
76
+
77
+ async createAccount(data: CreateAccountInput): Promise<Account> {
78
+ const [row] = await db
79
+ .insert(schema.accounts)
80
+ .values({
81
+ id: crypto.randomUUID(),
82
+ userId: data.userId,
83
+ provider: data.provider,
84
+ providerAccountId: data.providerAccountId,
85
+ accessToken: data.accessToken ?? null,
86
+ refreshToken: data.refreshToken ?? null,
87
+ idToken: data.idToken ?? null,
88
+ scope: data.scope ?? null,
89
+ tokenType: data.tokenType ?? null,
90
+ expiresAt: data.expiresAt ?? null,
91
+ createdAt: now(),
92
+ updatedAt: now(),
93
+ })
94
+ .returning()
95
+ return assertRow(row)
96
+ },
97
+
98
+ async getAccount(provider: string, providerAccountId: string): Promise<Account | null> {
99
+ const [row] = await db
100
+ .select()
101
+ .from(schema.accounts)
102
+ .where(
103
+ and(
104
+ eq(schema.accounts.provider, provider),
105
+ eq(schema.accounts.providerAccountId, providerAccountId),
106
+ ),
107
+ )
108
+ return row ?? null
109
+ },
110
+
111
+ async getAccountsByUserId(userId: string): Promise<Account[]> {
112
+ return db.select().from(schema.accounts).where(eq(schema.accounts.userId, userId))
113
+ },
114
+
115
+ async linkAccount(userId: string, data: CreateAccountInput): Promise<Account> {
116
+ const [row] = await db
117
+ .insert(schema.accounts)
118
+ .values({
119
+ id: crypto.randomUUID(),
120
+ userId,
121
+ provider: data.provider,
122
+ providerAccountId: data.providerAccountId,
123
+ accessToken: data.accessToken ?? null,
124
+ refreshToken: data.refreshToken ?? null,
125
+ idToken: data.idToken ?? null,
126
+ scope: data.scope ?? null,
127
+ tokenType: data.tokenType ?? null,
128
+ expiresAt: data.expiresAt ?? null,
129
+ createdAt: now(),
130
+ updatedAt: now(),
131
+ })
132
+ .returning()
133
+ return assertRow(row)
134
+ },
135
+
136
+ async deleteAccount(accountId: string): Promise<void> {
137
+ await db.delete(schema.accounts).where(eq(schema.accounts.id, accountId))
138
+ },
139
+
140
+ // --- Session operations ---
141
+
142
+ async createSession(data: CreateSessionInput): Promise<Session> {
143
+ const [row] = await db
144
+ .insert(schema.sessions)
145
+ .values({
146
+ id: crypto.randomUUID(),
147
+ userId: data.userId,
148
+ tokenHash: data.tokenHash,
149
+ expiresAt: data.expiresAt,
150
+ ipAddress: data.ipAddress ?? null,
151
+ userAgent: data.userAgent ?? null,
152
+ createdAt: now(),
153
+ updatedAt: now(),
154
+ })
155
+ .returning()
156
+ return assertRow(row)
157
+ },
158
+
159
+ async getSessionByTokenHash(tokenHash: string): Promise<SessionWithUser | null> {
160
+ const [row] = await db
161
+ .select()
162
+ .from(schema.sessions)
163
+ .where(eq(schema.sessions.tokenHash, tokenHash))
164
+ .leftJoin(schema.users, eq(schema.sessions.userId, schema.users.id))
165
+
166
+ if (!row) return null
167
+
168
+ const user = row.users
169
+ if (!user) return null
170
+
171
+ return {
172
+ session: row.sessions,
173
+ user,
174
+ }
175
+ },
176
+
177
+ async updateSession(id: string, data: UpdateSessionInput): Promise<Session> {
178
+ const [row] = await db
179
+ .update(schema.sessions)
180
+ .set({
181
+ ...data,
182
+ updatedAt: now(),
183
+ })
184
+ .where(eq(schema.sessions.id, id))
185
+ .returning()
186
+ return assertRow(row)
187
+ },
188
+
189
+ async deleteSession(tokenHash: string): Promise<void> {
190
+ await db.delete(schema.sessions).where(eq(schema.sessions.tokenHash, tokenHash))
191
+ },
192
+
193
+ async deleteExpiredSessions(): Promise<void> {
194
+ await db.delete(schema.sessions).where(sql`expires_at < ${Date.now()}`)
195
+ },
196
+
197
+ // --- OAuth state operations ---
198
+
199
+ async createOAuthState(data: CreateOAuthStateInput): Promise<void> {
200
+ await db.insert(schema.oauthStates).values({
201
+ id: crypto.randomUUID(),
202
+ stateHash: data.stateHash,
203
+ codeVerifier: data.codeVerifier,
204
+ provider: data.provider,
205
+ callbackUrl: data.callbackUrl ?? null,
206
+ errorUrl: data.errorUrl ?? null,
207
+ expiresAt: data.expiresAt,
208
+ createdAt: now(),
209
+ })
210
+ },
211
+
212
+ async consumeOAuthState(stateHash: string): Promise<OAuthState | null> {
213
+ const [state] = await db
214
+ .select()
215
+ .from(schema.oauthStates)
216
+ .where(eq(schema.oauthStates.stateHash, stateHash))
217
+
218
+ if (!state) return null
219
+
220
+ await db.delete(schema.oauthStates).where(eq(schema.oauthStates.stateHash, stateHash))
221
+
222
+ return state
223
+ },
224
+ }
225
+
226
+ return adapter
227
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { drizzleAdapter } from './adapter.js'
2
+ export * as schema from './schema.js'
3
+ export * as schemaPg from './schema-pg.js'
@@ -0,0 +1,62 @@
1
+ import { pgTable, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
2
+
3
+ export const users = pgTable('users', {
4
+ id: text('id').primaryKey(),
5
+ email: text('email'),
6
+ emailVerified: boolean('email_verified').default(false).notNull(),
7
+ name: text('name'),
8
+ image: text('image'),
9
+ createdAt: timestamp('created_at').notNull(),
10
+ updatedAt: timestamp('updated_at').notNull(),
11
+ })
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
14
+ export const accounts = pgTable(
15
+ 'accounts',
16
+ {
17
+ id: text('id').primaryKey(),
18
+ userId: text('user_id')
19
+ .notNull()
20
+ .references(() => users.id),
21
+ provider: text('provider').notNull(),
22
+ providerAccountId: text('provider_account_id').notNull(),
23
+ accessToken: text('access_token'),
24
+ refreshToken: text('refresh_token'),
25
+ idToken: text('id_token'),
26
+ scope: text('scope'),
27
+ tokenType: text('token_type'),
28
+ expiresAt: timestamp('expires_at'),
29
+ createdAt: timestamp('created_at').notNull(),
30
+ updatedAt: timestamp('updated_at').notNull(),
31
+ },
32
+ (table) => ({
33
+ providerAccountUnique: uniqueIndex('unique_provider_account').on(
34
+ table.provider,
35
+ table.providerAccountId,
36
+ ),
37
+ }),
38
+ )
39
+
40
+ export const sessions = pgTable('sessions', {
41
+ id: text('id').primaryKey(),
42
+ userId: text('user_id')
43
+ .notNull()
44
+ .references(() => users.id),
45
+ tokenHash: text('token_hash').unique().notNull(),
46
+ expiresAt: timestamp('expires_at').notNull(),
47
+ ipAddress: text('ip_address'),
48
+ userAgent: text('user_agent'),
49
+ createdAt: timestamp('created_at').notNull(),
50
+ updatedAt: timestamp('updated_at').notNull(),
51
+ })
52
+
53
+ export const oauthStates = pgTable('oauth_states', {
54
+ id: text('id').primaryKey(),
55
+ stateHash: text('state_hash').unique().notNull(),
56
+ codeVerifier: text('code_verifier').notNull(),
57
+ provider: text('provider').notNull(),
58
+ callbackUrl: text('callback_url'),
59
+ errorUrl: text('error_url'),
60
+ expiresAt: timestamp('expires_at').notNull(),
61
+ createdAt: timestamp('created_at').notNull(),
62
+ })
package/src/schema.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core'
2
+
3
+ export const users = sqliteTable('users', {
4
+ id: text('id').primaryKey(),
5
+ email: text('email'),
6
+ emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
7
+ name: text('name'),
8
+ image: text('image'),
9
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
10
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
11
+ })
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
14
+ export const accounts = sqliteTable(
15
+ 'accounts',
16
+ {
17
+ id: text('id').primaryKey(),
18
+ userId: text('user_id')
19
+ .notNull()
20
+ .references(() => users.id),
21
+ provider: text('provider').notNull(),
22
+ providerAccountId: text('provider_account_id').notNull(),
23
+ accessToken: text('access_token'),
24
+ refreshToken: text('refresh_token'),
25
+ idToken: text('id_token'),
26
+ scope: text('scope'),
27
+ tokenType: text('token_type'),
28
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
29
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
30
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
31
+ },
32
+ (table) => ({
33
+ providerAccountUnique: uniqueIndex('unique_provider_account').on(
34
+ table.provider,
35
+ table.providerAccountId,
36
+ ),
37
+ }),
38
+ )
39
+
40
+ export const sessions = sqliteTable('sessions', {
41
+ id: text('id').primaryKey(),
42
+ userId: text('user_id')
43
+ .notNull()
44
+ .references(() => users.id),
45
+ tokenHash: text('token_hash').unique().notNull(),
46
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
47
+ ipAddress: text('ip_address'),
48
+ userAgent: text('user_agent'),
49
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
50
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
51
+ })
52
+
53
+ export const oauthStates = sqliteTable('oauth_states', {
54
+ id: text('id').primaryKey(),
55
+ stateHash: text('state_hash').unique().notNull(),
56
+ codeVerifier: text('code_verifier').notNull(),
57
+ provider: text('provider').notNull(),
58
+ callbackUrl: text('callback_url'),
59
+ errorUrl: text('error_url'),
60
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
61
+ createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
62
+ })
@@ -0,0 +1,93 @@
1
+ import { describe, beforeAll, afterAll, beforeEach } from 'vitest'
2
+ import { createClient } from '@libsql/client'
3
+ import { runAdapterConformanceTests } from '@cloudauth/core/test'
4
+ import { drizzleAdapter } from '../adapter.js'
5
+ import * as schema from '../schema.js'
6
+ import { drizzle } from 'drizzle-orm/libsql'
7
+
8
+ describe('Drizzle SQLite adapter', () => {
9
+ let client: ReturnType<typeof createClient>
10
+
11
+ beforeAll(async () => {
12
+ client = createClient({
13
+ url: ':memory:',
14
+ })
15
+
16
+ // Create tables once
17
+ await client.execute(`
18
+ CREATE TABLE IF NOT EXISTS users (
19
+ id text PRIMARY KEY,
20
+ email text,
21
+ email_verified integer DEFAULT 0 NOT NULL,
22
+ name text,
23
+ image text,
24
+ created_at integer NOT NULL,
25
+ updated_at integer NOT NULL
26
+ )
27
+ `)
28
+ await client.execute(`
29
+ CREATE TABLE IF NOT EXISTS accounts (
30
+ id text PRIMARY KEY,
31
+ user_id text NOT NULL REFERENCES users(id),
32
+ provider text NOT NULL,
33
+ provider_account_id text NOT NULL,
34
+ access_token text,
35
+ refresh_token text,
36
+ id_token text,
37
+ scope text,
38
+ token_type text,
39
+ expires_at integer,
40
+ created_at integer NOT NULL,
41
+ updated_at integer NOT NULL
42
+ )
43
+ `)
44
+ await client.execute(`
45
+ CREATE UNIQUE INDEX IF NOT EXISTS unique_provider_account
46
+ ON accounts(provider, provider_account_id)
47
+ `)
48
+ await client.execute(`
49
+ CREATE TABLE IF NOT EXISTS sessions (
50
+ id text PRIMARY KEY,
51
+ user_id text NOT NULL REFERENCES users(id),
52
+ token_hash text UNIQUE NOT NULL,
53
+ expires_at integer NOT NULL,
54
+ ip_address text,
55
+ user_agent text,
56
+ created_at integer NOT NULL,
57
+ updated_at integer NOT NULL
58
+ )
59
+ `)
60
+ await client.execute(`
61
+ CREATE TABLE IF NOT EXISTS oauth_states (
62
+ id text PRIMARY KEY,
63
+ state_hash text UNIQUE NOT NULL,
64
+ code_verifier text NOT NULL,
65
+ provider text NOT NULL,
66
+ callback_url text,
67
+ error_url text,
68
+ expires_at integer NOT NULL,
69
+ created_at integer NOT NULL
70
+ )
71
+ `)
72
+ })
73
+
74
+ beforeEach(async () => {
75
+ // Clean data between tests to ensure test isolation
76
+ await client.execute('DELETE FROM accounts')
77
+ await client.execute('DELETE FROM sessions')
78
+ await client.execute('DELETE FROM oauth_states')
79
+ await client.execute('DELETE FROM users')
80
+ })
81
+
82
+ afterAll(() => {
83
+ client.close()
84
+ })
85
+
86
+ runAdapterConformanceTests({
87
+ name: 'SQLite (libsql)',
88
+ createAdapter: () => {
89
+ const db = drizzle(client, { schema })
90
+ return drizzleAdapter(db)
91
+ },
92
+ })
93
+ })
@@ -0,0 +1,211 @@
1
+ import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest'
2
+ import { createClient } from '@libsql/client'
3
+ import { drizzle } from 'drizzle-orm/libsql'
4
+ import { drizzleAdapter } from '../adapter.js'
5
+ import * as schema from '../schema.js'
6
+ import { cloudAuth } from '@cloudauth/core'
7
+ import type {
8
+ AuthProvider,
9
+ AuthorizationParams,
10
+ ExchangeCodeParams,
11
+ TokenSet,
12
+ ProviderProfile,
13
+ } from '@cloudauth/core'
14
+
15
+ // A mock provider for testing
16
+ const mockProvider: AuthProvider = {
17
+ id: 'mock',
18
+ type: 'oauth',
19
+ authorizationUrl(params: AuthorizationParams): URL {
20
+ const url = new URL('https://mock.provider/oauth/authorize')
21
+ url.searchParams.set('state', params.state)
22
+ url.searchParams.set('code_challenge', params.codeVerifier)
23
+ url.searchParams.set('redirect_uri', params.redirectUri)
24
+ return url
25
+ },
26
+ exchangeCode(_params: ExchangeCodeParams): Promise<TokenSet> {
27
+ return Promise.resolve({
28
+ accessToken: 'mock_access_token',
29
+ refreshToken: 'mock_refresh_token',
30
+ scope: 'email profile',
31
+ tokenType: 'Bearer',
32
+ expiresAt: 3600,
33
+ })
34
+ },
35
+ getProfile(_tokens: TokenSet): Promise<ProviderProfile> {
36
+ return Promise.resolve({
37
+ provider: 'mock',
38
+ providerAccountId: 'mock_user_123',
39
+ email: 'mock@example.com',
40
+ emailVerified: true,
41
+ name: 'Mock User',
42
+ image: 'https://example.com/avatar.png',
43
+ })
44
+ },
45
+ }
46
+
47
+ function getCookieValue(cookieHeader: string): string | null {
48
+ const firstPart = cookieHeader.split(';')[0]
49
+ if (!firstPart) return null
50
+ const eqIndex = firstPart.indexOf('=')
51
+ if (eqIndex === -1) return null
52
+ return firstPart.slice(eqIndex + 1)
53
+ }
54
+
55
+ describe('cloudAuth integration', () => {
56
+ let client: ReturnType<typeof createClient>
57
+ let auth: ReturnType<typeof cloudAuth>
58
+
59
+ beforeAll(async () => {
60
+ client = createClient({ url: ':memory:' })
61
+
62
+ await client.execute(`
63
+ CREATE TABLE IF NOT EXISTS users (
64
+ id text PRIMARY KEY, email text, email_verified integer DEFAULT 0 NOT NULL,
65
+ name text, image text, created_at integer NOT NULL, updated_at integer NOT NULL
66
+ )
67
+ `)
68
+ await client.execute(`
69
+ CREATE TABLE IF NOT EXISTS accounts (
70
+ id text PRIMARY KEY, user_id text NOT NULL REFERENCES users(id),
71
+ provider text NOT NULL, provider_account_id text NOT NULL,
72
+ access_token text, refresh_token text, id_token text,
73
+ scope text, token_type text, expires_at integer,
74
+ created_at integer NOT NULL, updated_at integer NOT NULL
75
+ )
76
+ `)
77
+ await client.execute(`
78
+ CREATE UNIQUE INDEX IF NOT EXISTS unique_provider_account
79
+ ON accounts(provider, provider_account_id)
80
+ `)
81
+ await client.execute(`
82
+ CREATE TABLE IF NOT EXISTS sessions (
83
+ id text PRIMARY KEY, user_id text NOT NULL REFERENCES users(id),
84
+ token_hash text UNIQUE NOT NULL, expires_at integer NOT NULL,
85
+ ip_address text, user_agent text,
86
+ created_at integer NOT NULL, updated_at integer NOT NULL
87
+ )
88
+ `)
89
+ await client.execute(`
90
+ CREATE TABLE IF NOT EXISTS oauth_states (
91
+ id text PRIMARY KEY, state_hash text UNIQUE NOT NULL,
92
+ code_verifier text NOT NULL, provider text NOT NULL,
93
+ callback_url text, error_url text,
94
+ expires_at integer NOT NULL, created_at integer NOT NULL
95
+ )
96
+ `)
97
+ })
98
+
99
+ beforeEach(async () => {
100
+ await client.execute('DELETE FROM accounts')
101
+ await client.execute('DELETE FROM sessions')
102
+ await client.execute('DELETE FROM oauth_states')
103
+ await client.execute('DELETE FROM users')
104
+
105
+ const db = drizzle(client, { schema })
106
+ auth = cloudAuth({
107
+ appName: 'TestApp',
108
+ baseURL: 'https://test.example.com',
109
+ providers: [mockProvider],
110
+ database: drizzleAdapter(db),
111
+ security: { secret: 'test-secret-that-is-at-least-32-characters!!' },
112
+ })
113
+ })
114
+
115
+ afterAll(() => {
116
+ client.close()
117
+ })
118
+
119
+ it('should create an authorization URL', async () => {
120
+ const url = await auth.createAuthUrl('mock')
121
+ expect(url).toBeInstanceOf(URL)
122
+ expect(url.hostname).toBe('mock.provider')
123
+ expect(url.searchParams.has('state')).toBe(true)
124
+ expect(url.searchParams.has('code_challenge')).toBe(true)
125
+ expect(url.searchParams.has('redirect_uri')).toBe(true)
126
+ })
127
+
128
+ it('should handle a full sign-in callback', async () => {
129
+ const authUrl = await auth.createAuthUrl('mock')
130
+ const stateParam = authUrl.searchParams.get('state')
131
+ expect(stateParam).toBeTruthy()
132
+
133
+ const callbackUrl = `https://test.example.com/auth/mock/callback?code=mock_code&state=${String(stateParam)}`
134
+ const result = await auth.handleCallback('mock', new Request(callbackUrl))
135
+
136
+ expect(result.user).toBeDefined()
137
+ expect(result.user.email).toBe('mock@example.com')
138
+ expect(result.session).toBeDefined()
139
+ expect(result.cookieHeader).toContain('cloud_auth_session')
140
+ expect(result.cookieHeader).toContain('HttpOnly')
141
+ })
142
+
143
+ it('should get a valid session after sign-in', async () => {
144
+ const authUrl = await auth.createAuthUrl('mock')
145
+ const stateParam = authUrl.searchParams.get('state')
146
+ expect(stateParam).toBeTruthy()
147
+ const callbackUrl = `https://test.example.com/auth/mock/callback?code=mock_code&state=${String(stateParam)}`
148
+ const result = await auth.handleCallback('mock', new Request(callbackUrl))
149
+
150
+ const cookieValue = getCookieValue(result.cookieHeader)
151
+ expect(cookieValue).toBeTruthy()
152
+
153
+ const sessionRequest = new Request('https://test.example.com/dashboard', {
154
+ headers: { cookie: `cloud_auth_session=${String(cookieValue)}` },
155
+ })
156
+
157
+ const session = await auth.getSession(sessionRequest)
158
+ expect(session).not.toBeNull()
159
+ if (session) {
160
+ expect(session.user.email).toBe('mock@example.com')
161
+ }
162
+ })
163
+
164
+ it('should return null for unauthenticated requests', async () => {
165
+ const req = new Request('https://test.example.com/protected')
166
+ const session = await auth.getSession(req)
167
+ expect(session).toBeNull()
168
+ })
169
+
170
+ it('should sign out and clear the session', async () => {
171
+ const authUrl = await auth.createAuthUrl('mock')
172
+ const stateParam = authUrl.searchParams.get('state')
173
+ expect(stateParam).toBeTruthy()
174
+ const callbackUrl = `https://test.example.com/auth/mock/callback?code=mock_code&state=${String(stateParam)}`
175
+ const result = await auth.handleCallback('mock', new Request(callbackUrl))
176
+ const cookieValue = getCookieValue(result.cookieHeader)
177
+ expect(cookieValue).toBeTruthy()
178
+
179
+ const signOutReq = new Request('https://test.example.com/logout', {
180
+ headers: { cookie: `cloud_auth_session=${String(cookieValue)}` },
181
+ })
182
+ const clearCookie = await auth.signOut(signOutReq)
183
+
184
+ expect(clearCookie).toContain('Max-Age=0')
185
+
186
+ const checkReq = new Request('https://test.example.com/dashboard', {
187
+ headers: { cookie: `cloud_auth_session=${String(cookieValue)}` },
188
+ })
189
+ const session = await auth.getSession(checkReq)
190
+ expect(session).toBeNull()
191
+ })
192
+
193
+ it('should throw on invalid state', async () => {
194
+ const callbackUrl = 'https://test.example.com/auth/mock/callback?code=bad&state=invalid'
195
+ await expect(auth.handleCallback('mock', new Request(callbackUrl))).rejects.toThrow()
196
+ })
197
+
198
+ it('should throw on missing security secret', async () => {
199
+ const db = drizzle(client, { schema })
200
+ const badAuth = cloudAuth({
201
+ appName: 'Test',
202
+ baseURL: 'https://test.com',
203
+ providers: [mockProvider],
204
+ database: drizzleAdapter(db),
205
+ })
206
+
207
+ await expect(badAuth.getSession(new Request('https://test.com'))).rejects.toThrow(
208
+ 'security.secret is required',
209
+ )
210
+ })
211
+ })