myaidev-method 0.2.8 → 0.2.9

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 (157) hide show
  1. package/.claude/agents/wordpress-admin.md +271 -0
  2. package/.env.example +0 -1
  3. package/PACKAGE_FIXES_SUMMARY.md +319 -0
  4. package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
  5. package/USER_GUIDE.md +260 -0
  6. package/bin/cli.js +36 -0
  7. package/dist/server/.tsbuildinfo +1 -0
  8. package/dist/server/auth/controllers/AuthController.d.ts +34 -0
  9. package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
  10. package/dist/server/auth/controllers/AuthController.js +43 -0
  11. package/dist/server/auth/controllers/AuthController.js.map +1 -0
  12. package/dist/server/auth/example-usage.d.ts +53 -0
  13. package/dist/server/auth/example-usage.d.ts.map +1 -0
  14. package/dist/server/auth/example-usage.js +129 -0
  15. package/dist/server/auth/example-usage.js.map +1 -0
  16. package/dist/server/auth/index.d.ts +11 -0
  17. package/dist/server/auth/index.d.ts.map +1 -0
  18. package/dist/server/auth/index.js +15 -0
  19. package/dist/server/auth/index.js.map +1 -0
  20. package/dist/server/auth/layers.d.ts +19 -0
  21. package/dist/server/auth/layers.d.ts.map +1 -0
  22. package/dist/server/auth/layers.js +33 -0
  23. package/dist/server/auth/layers.js.map +1 -0
  24. package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
  25. package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
  26. package/dist/server/auth/middleware/authMiddleware.js +65 -0
  27. package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
  28. package/dist/server/auth/routes/authRoutes.d.ts +11 -0
  29. package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
  30. package/dist/server/auth/routes/authRoutes.js +213 -0
  31. package/dist/server/auth/routes/authRoutes.js.map +1 -0
  32. package/dist/server/auth/services/AuditLogService.d.ts +21 -0
  33. package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
  34. package/dist/server/auth/services/AuditLogService.js +28 -0
  35. package/dist/server/auth/services/AuditLogService.js.map +1 -0
  36. package/dist/server/auth/services/AuthService.d.ts +27 -0
  37. package/dist/server/auth/services/AuthService.d.ts.map +1 -0
  38. package/dist/server/auth/services/AuthService.js +246 -0
  39. package/dist/server/auth/services/AuthService.js.map +1 -0
  40. package/dist/server/auth/services/PasswordService.d.ts +12 -0
  41. package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
  42. package/dist/server/auth/services/PasswordService.js +31 -0
  43. package/dist/server/auth/services/PasswordService.js.map +1 -0
  44. package/dist/server/auth/services/SessionRepository.d.ts +24 -0
  45. package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
  46. package/dist/server/auth/services/SessionRepository.js +101 -0
  47. package/dist/server/auth/services/SessionRepository.js.map +1 -0
  48. package/dist/server/auth/services/TokenService.d.ts +12 -0
  49. package/dist/server/auth/services/TokenService.d.ts.map +1 -0
  50. package/dist/server/auth/services/TokenService.js +86 -0
  51. package/dist/server/auth/services/TokenService.js.map +1 -0
  52. package/dist/server/auth/services/UserRepository.d.ts +23 -0
  53. package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
  54. package/dist/server/auth/services/UserRepository.js +168 -0
  55. package/dist/server/auth/services/UserRepository.js.map +1 -0
  56. package/dist/server/auth/services/example.d.ts +26 -0
  57. package/dist/server/auth/services/example.d.ts.map +1 -0
  58. package/dist/server/auth/services/example.js +221 -0
  59. package/dist/server/auth/services/example.js.map +1 -0
  60. package/dist/server/auth/services/index.d.ts +6 -0
  61. package/dist/server/auth/services/index.d.ts.map +1 -0
  62. package/dist/server/auth/services/index.js +7 -0
  63. package/dist/server/auth/services/index.js.map +1 -0
  64. package/dist/server/database/db.d.ts +28 -0
  65. package/dist/server/database/db.d.ts.map +1 -0
  66. package/dist/server/database/db.js +91 -0
  67. package/dist/server/database/db.js.map +1 -0
  68. package/dist/server/database/schema.sql +95 -0
  69. package/dist/server/hono/app.d.ts +10 -0
  70. package/dist/server/hono/app.d.ts.map +1 -0
  71. package/dist/server/hono/app.js +26 -0
  72. package/dist/server/hono/app.js.map +1 -0
  73. package/dist/server/hono/routes.d.ts +12 -0
  74. package/dist/server/hono/routes.d.ts.map +1 -0
  75. package/dist/server/hono/routes.js +40 -0
  76. package/dist/server/hono/routes.js.map +1 -0
  77. package/dist/server/main.d.ts +2 -0
  78. package/dist/server/main.d.ts.map +1 -0
  79. package/dist/server/main.js +94 -0
  80. package/dist/server/main.js.map +1 -0
  81. package/dist/server/user-management/DirectoryService.d.ts +62 -0
  82. package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
  83. package/dist/server/user-management/DirectoryService.js +201 -0
  84. package/dist/server/user-management/DirectoryService.js.map +1 -0
  85. package/dist/server/user-management/LinuxUserService.d.ts +71 -0
  86. package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
  87. package/dist/server/user-management/LinuxUserService.js +192 -0
  88. package/dist/server/user-management/LinuxUserService.js.map +1 -0
  89. package/dist/server/user-management/QuotaService.d.ts +59 -0
  90. package/dist/server/user-management/QuotaService.d.ts.map +1 -0
  91. package/dist/server/user-management/QuotaService.js +148 -0
  92. package/dist/server/user-management/QuotaService.js.map +1 -0
  93. package/dist/server/user-management/UserManagementService.d.ts +74 -0
  94. package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
  95. package/dist/server/user-management/UserManagementService.js +122 -0
  96. package/dist/server/user-management/UserManagementService.js.map +1 -0
  97. package/dist/server/user-management/index.d.ts +26 -0
  98. package/dist/server/user-management/index.d.ts.map +1 -0
  99. package/dist/server/user-management/index.js +26 -0
  100. package/dist/server/user-management/index.js.map +1 -0
  101. package/dist/server/user-management/layers.d.ts +27 -0
  102. package/dist/server/user-management/layers.d.ts.map +1 -0
  103. package/dist/server/user-management/layers.js +37 -0
  104. package/dist/server/user-management/layers.js.map +1 -0
  105. package/dist/shared/types.d.ts +94 -0
  106. package/dist/shared/types.d.ts.map +1 -0
  107. package/dist/shared/types.js +32 -0
  108. package/dist/shared/types.js.map +1 -0
  109. package/package.json +25 -5
  110. package/src/lib/payloadcms-utils.js +5 -12
  111. package/src/server/auth/ARCHITECTURE.md +575 -0
  112. package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
  113. package/src/server/auth/QUICK_START.md +283 -0
  114. package/src/server/auth/README.md +290 -0
  115. package/src/server/auth/controllers/AuthController.ts +129 -0
  116. package/src/server/auth/example-usage.ts +159 -0
  117. package/src/server/auth/index.ts +19 -0
  118. package/src/server/auth/layers.ts +57 -0
  119. package/src/server/auth/middleware/authMiddleware.ts +118 -0
  120. package/src/server/auth/routes/authRoutes.ts +319 -0
  121. package/src/server/auth/services/AuditLogService.ts +81 -0
  122. package/src/server/auth/services/AuthService.ts +408 -0
  123. package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
  124. package/src/server/auth/services/PasswordService.ts +85 -0
  125. package/src/server/auth/services/README.md +361 -0
  126. package/src/server/auth/services/SessionRepository.ts +227 -0
  127. package/src/server/auth/services/TokenService.ts +174 -0
  128. package/src/server/auth/services/UserRepository.ts +318 -0
  129. package/src/server/auth/services/example.ts +346 -0
  130. package/src/server/auth/services/index.ts +6 -0
  131. package/src/server/database/db.ts +161 -0
  132. package/src/server/database/schema.sql +95 -0
  133. package/src/server/hono/app.ts +41 -0
  134. package/src/server/main.ts +115 -0
  135. package/src/server/user-management/DirectoryService.ts +348 -0
  136. package/src/server/user-management/LinuxUserService.ts +338 -0
  137. package/src/server/user-management/QuotaService.ts +256 -0
  138. package/src/server/user-management/README.md +333 -0
  139. package/src/server/user-management/UserManagementService.ts +335 -0
  140. package/src/server/user-management/index.ts +26 -0
  141. package/src/server/user-management/layers.ts +51 -0
  142. package/src/shared/types.ts +111 -0
  143. package/src/templates/claude/agents/payloadcms-publish.md +34 -14
  144. package/src/templates/codex/commands/myai-astro-publish.md +8 -2
  145. package/src/templates/codex/commands/myai-content-writer.md +8 -2
  146. package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
  147. package/src/templates/codex/commands/myai-dev-architect.md +8 -2
  148. package/src/templates/codex/commands/myai-dev-code.md +8 -2
  149. package/src/templates/codex/commands/myai-dev-docs.md +8 -2
  150. package/src/templates/codex/commands/myai-dev-review.md +8 -2
  151. package/src/templates/codex/commands/myai-dev-test.md +8 -2
  152. package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
  153. package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
  154. package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
  155. package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
  156. package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
  157. package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
@@ -0,0 +1,361 @@
1
+ # Authentication Services
2
+
3
+ This directory contains Effect-TS based authentication services following the ccviewer codebase patterns.
4
+
5
+ ## Architecture
6
+
7
+ All services follow these principles:
8
+
9
+ - **Effect-TS Only**: No async/await, no classes except Context.Tag
10
+ - **No Type Casting**: No `as` type casting allowed
11
+ - **Functional Composition**: Use Effect.gen with yield* for composition
12
+ - **Proper Error Handling**: Typed errors (AuthError, ValidationError, DatabaseError)
13
+ - **Layer-based DI**: Context.Tag with Layer.effect or Layer.succeed
14
+
15
+ ## Services
16
+
17
+ ### 1. PasswordService
18
+
19
+ Handles password hashing and validation.
20
+
21
+ ```typescript
22
+ import { PasswordService } from "./services/PasswordService.js";
23
+
24
+ const program = Effect.gen(function* (_) {
25
+ const passwordService = yield* _(PasswordService);
26
+
27
+ // Validate password strength
28
+ yield* _(passwordService.validatePasswordStrength("SecurePass123"));
29
+
30
+ // Hash password
31
+ const hash = yield* _(passwordService.hash("SecurePass123"));
32
+
33
+ // Verify password
34
+ const isValid = yield* _(passwordService.verify("SecurePass123", hash));
35
+ });
36
+ ```
37
+
38
+ **Methods:**
39
+ - `hash(password: string)` - Hash password with bcrypt (cost 12)
40
+ - `verify(password: string, hash: string)` - Verify password against hash
41
+ - `validatePasswordStrength(password: string)` - Validate password requirements
42
+
43
+ **Password Requirements:**
44
+ - Minimum 8 characters
45
+ - At least 1 uppercase letter
46
+ - At least 1 lowercase letter
47
+ - At least 1 number
48
+
49
+ ### 2. TokenService
50
+
51
+ JWT token generation and verification using RS256 algorithm.
52
+
53
+ ```typescript
54
+ import { TokenService } from "./services/TokenService.js";
55
+
56
+ const program = Effect.gen(function* (_) {
57
+ const tokenService = yield* _(TokenService);
58
+
59
+ // Generate JWT token
60
+ const token = yield* _(tokenService.generateToken({
61
+ sub: "user-id",
62
+ username: "johndoe",
63
+ email: "john@example.com",
64
+ jti: "session-id"
65
+ }));
66
+
67
+ // Verify token
68
+ const payload = yield* _(tokenService.verifyToken(token));
69
+
70
+ // Hash token for database storage
71
+ const tokenHash = yield* _(tokenService.hashToken(token));
72
+ });
73
+ ```
74
+
75
+ **Methods:**
76
+ - `generateToken(payload)` - Generate JWT with RS256 (7 day expiry)
77
+ - `verifyToken(token)` - Verify token signature and expiration
78
+ - `hashToken(token)` - SHA-256 hash for database storage
79
+
80
+ **Key Generation:**
81
+ - RS256 key pair generated at service initialization
82
+ - Keys stored in memory (can be modified to use environment variables)
83
+
84
+ ### 3. UserRepository
85
+
86
+ User CRUD operations with database integration.
87
+
88
+ ```typescript
89
+ import { UserRepository } from "./services/UserRepository.js";
90
+
91
+ const program = Effect.gen(function* (_) {
92
+ const userRepo = yield* _(UserRepository);
93
+
94
+ // Create user
95
+ const user = yield* _(userRepo.create({
96
+ username: "johndoe",
97
+ email: "john@example.com",
98
+ passwordHash: "hashed-password",
99
+ linuxUsername: "john-doe"
100
+ }));
101
+
102
+ // Find by email
103
+ const found = yield* _(userRepo.findByEmail("john@example.com"));
104
+
105
+ // Update user
106
+ const updated = yield* _(userRepo.update(user.id, {
107
+ emailVerified: true
108
+ }));
109
+
110
+ // Manage failed logins
111
+ yield* _(userRepo.incrementFailedLogins(user.id));
112
+ yield* _(userRepo.resetFailedLogins(user.id));
113
+ });
114
+ ```
115
+
116
+ **Methods:**
117
+ - `create(data)` - Create new user with generated UUID
118
+ - `findById(id)` - Find user by ID
119
+ - `findByEmail(email)` - Find user by email
120
+ - `findByUsername(username)` - Find user by username
121
+ - `update(id, data)` - Update user fields
122
+ - `incrementFailedLogins(id)` - Increment failed login counter
123
+ - `resetFailedLogins(id)` - Reset failed login counter
124
+
125
+ ### 4. SessionRepository
126
+
127
+ Session management with token hash storage.
128
+
129
+ ```typescript
130
+ import { SessionRepository } from "./services/SessionRepository.js";
131
+
132
+ const program = Effect.gen(function* (_) {
133
+ const sessionRepo = yield* _(SessionRepository);
134
+
135
+ // Create session
136
+ const session = yield* _(sessionRepo.create({
137
+ userId: "user-id",
138
+ tokenHash: "sha256-hash",
139
+ ipAddress: "127.0.0.1",
140
+ userAgent: "Mozilla/5.0",
141
+ expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000
142
+ }));
143
+
144
+ // Find by token hash
145
+ const found = yield* _(sessionRepo.findByTokenHash("sha256-hash"));
146
+
147
+ // Revoke session
148
+ yield* _(sessionRepo.revoke(session.id));
149
+
150
+ // Revoke all user sessions
151
+ yield* _(sessionRepo.revokeAllForUser("user-id"));
152
+
153
+ // Clean up expired sessions
154
+ const deletedCount = yield* _(sessionRepo.deleteExpired());
155
+ });
156
+ ```
157
+
158
+ **Methods:**
159
+ - `create(data)` - Create new session with generated UUID
160
+ - `findById(id)` - Find session by ID
161
+ - `findByTokenHash(tokenHash)` - Find session by token hash
162
+ - `revoke(id)` - Revoke single session
163
+ - `revokeAllForUser(userId)` - Revoke all user sessions
164
+ - `deleteExpired()` - Delete expired sessions (returns count)
165
+
166
+ ### 5. AuditLogService
167
+
168
+ Audit logging for security and compliance.
169
+
170
+ ```typescript
171
+ import { AuditLogService } from "./services/AuditLogService.js";
172
+
173
+ const program = Effect.gen(function* (_) {
174
+ const auditLog = yield* _(AuditLogService);
175
+
176
+ // Log user action
177
+ yield* _(auditLog.log({
178
+ userId: "user-id",
179
+ action: "USER_LOGIN",
180
+ resourceType: "session",
181
+ resourceId: "session-id",
182
+ ipAddress: "127.0.0.1",
183
+ userAgent: "Mozilla/5.0",
184
+ details: JSON.stringify({ method: "password" })
185
+ }));
186
+ });
187
+ ```
188
+
189
+ **Methods:**
190
+ - `log(data)` - Create audit log entry
191
+
192
+ **Predefined Actions:**
193
+ - `USER_REGISTERED` - New user registration
194
+ - `USER_LOGIN` - Successful login
195
+ - `USER_LOGOUT` - User logout
196
+ - `LOGIN_FAILED` - Failed login attempt
197
+ - `PASSWORD_CHANGED` - Password update
198
+ - `PASSWORD_RESET_REQUESTED` - Password reset initiated
199
+ - `PASSWORD_RESET_COMPLETED` - Password reset completed
200
+ - `EMAIL_VERIFIED` - Email verification
201
+ - `EMAIL_CHANGED` - Email address update
202
+ - `PROFILE_UPDATED` - Profile information update
203
+ - `ACCOUNT_LOCKED` - Account locked
204
+ - `ACCOUNT_UNLOCKED` - Account unlocked
205
+ - `SESSION_CREATED` - New session created
206
+ - `SESSION_REVOKED` - Session revoked
207
+ - `TOKEN_REFRESHED` - Token refreshed
208
+ - `OAUTH_LINKED` - OAuth provider linked
209
+ - `OAUTH_UNLINKED` - OAuth provider unlinked
210
+ - `TWO_FACTOR_ENABLED` - 2FA enabled
211
+ - `TWO_FACTOR_DISABLED` - 2FA disabled
212
+
213
+ ## Layer Composition
214
+
215
+ Create a complete authentication layer:
216
+
217
+ ```typescript
218
+ import { Layer } from "effect";
219
+ import { DatabaseService } from "../../database/db.js";
220
+ import {
221
+ PasswordService,
222
+ TokenService,
223
+ UserRepository,
224
+ SessionRepository,
225
+ AuditLogService
226
+ } from "./services/index.js";
227
+
228
+ // Combine authentication services
229
+ const AuthenticationLayer = Layer.mergeAll(
230
+ PasswordService.Live,
231
+ TokenService.Live,
232
+ UserRepository.Live,
233
+ SessionRepository.Live,
234
+ AuditLogService.Live
235
+ );
236
+
237
+ // Complete application layer with database
238
+ const AppLayer = Layer.mergeAll(
239
+ DatabaseService.Live({ path: "./app.db" }),
240
+ AuthenticationLayer
241
+ );
242
+
243
+ // Use in your program
244
+ const program = Effect.gen(function* (_) {
245
+ // Your authentication logic here
246
+ });
247
+
248
+ Effect.runPromise(program.pipe(Effect.provide(AppLayer)));
249
+ ```
250
+
251
+ ## Complete Examples
252
+
253
+ See `example.ts` for complete authentication flows:
254
+
255
+ 1. **User Registration Flow**
256
+ - Password validation
257
+ - Password hashing
258
+ - User creation
259
+ - Audit logging
260
+
261
+ 2. **Login Flow**
262
+ - User lookup
263
+ - Password verification
264
+ - Token generation
265
+ - Session creation
266
+ - Failed login tracking
267
+ - Audit logging
268
+
269
+ 3. **Token Verification Flow**
270
+ - JWT verification
271
+ - Session validation
272
+ - User status check
273
+
274
+ 4. **Logout Flow**
275
+ - Session revocation
276
+ - Audit logging
277
+
278
+ ## Database Schema
279
+
280
+ All services require these database tables (already defined in `src/server/database/schema.sql`):
281
+
282
+ - `users` - User accounts
283
+ - `sessions` - Active sessions with token hashes
284
+ - `audit_logs` - Security audit trail
285
+
286
+ ## Error Handling
287
+
288
+ All services use typed errors from `src/shared/types.ts`:
289
+
290
+ - **AuthError** - Authentication/authorization failures
291
+ - **ValidationError** - Input validation failures
292
+ - **DatabaseError** - Database operation failures
293
+
294
+ ```typescript
295
+ import { AuthError, ValidationError, DatabaseError } from "../../../shared/types.js";
296
+
297
+ const program = Effect.gen(function* (_) {
298
+ // Handle specific error types
299
+ }).pipe(
300
+ Effect.catchTag("AuthError", (error) =>
301
+ Effect.succeed({ error: error.message })
302
+ ),
303
+ Effect.catchTag("ValidationError", (error) =>
304
+ Effect.succeed({ field: error.field, error: error.message })
305
+ )
306
+ );
307
+ ```
308
+
309
+ ## Testing
310
+
311
+ All services can be tested using Effect test layers:
312
+
313
+ ```typescript
314
+ import { expect, test } from "vitest";
315
+ import { Effect, Layer } from "effect";
316
+ import { DatabaseService } from "../../database/db.js";
317
+ import { PasswordService } from "./PasswordService.js";
318
+
319
+ test("password hashing and verification", async () => {
320
+ const program = Effect.gen(function* (_) {
321
+ const passwordService = yield* _(PasswordService);
322
+ const hash = yield* _(passwordService.hash("SecurePass123"));
323
+ const isValid = yield* _(passwordService.verify("SecurePass123", hash));
324
+ return isValid;
325
+ });
326
+
327
+ const testLayer = Layer.mergeAll(
328
+ DatabaseService.Live({ path: ":memory:" }),
329
+ PasswordService.Live
330
+ );
331
+
332
+ const result = await Effect.runPromise(
333
+ program.pipe(Effect.provide(testLayer))
334
+ );
335
+
336
+ expect(result).toBe(true);
337
+ });
338
+ ```
339
+
340
+ ## Security Considerations
341
+
342
+ 1. **Password Storage**: Bcrypt with cost factor 12
343
+ 2. **Token Storage**: Never store raw JWT tokens, use SHA-256 hash
344
+ 3. **Session Management**: Track and revoke sessions
345
+ 4. **Audit Trail**: Log all security-relevant actions
346
+ 5. **Failed Login Tracking**: Monitor and limit failed attempts
347
+ 6. **Token Expiry**: 7-day expiration (configurable)
348
+
349
+ ## Dependencies
350
+
351
+ - `effect` - Effect-TS runtime
352
+ - `bcrypt` - Password hashing
353
+ - `jose` - JWT operations
354
+ - `better-sqlite3` - Database (via DatabaseService)
355
+ - `node:crypto` - UUID generation and SHA-256 hashing
356
+
357
+ ## References
358
+
359
+ - Effect-TS documentation: https://effect.website/
360
+ - ccviewer patterns: `/home/ubuntu/projects/myaidev-method/ccviewer/`
361
+ - Database schema: `/home/ubuntu/projects/myaidev-method/src/server/database/schema.sql`
@@ -0,0 +1,227 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { randomUUID } from "node:crypto";
3
+ import { DatabaseService } from "../../database/db.js";
4
+ import { DatabaseError, Session } from "../../../shared/types.js";
5
+
6
+ export interface CreateSessionData {
7
+ userId: string;
8
+ tokenHash: string;
9
+ ipAddress: string | null;
10
+ userAgent: string | null;
11
+ expiresAt: number;
12
+ }
13
+
14
+ export class SessionRepository extends Context.Tag("SessionRepository")<
15
+ SessionRepository,
16
+ {
17
+ readonly create: (
18
+ data: CreateSessionData
19
+ ) => Effect.Effect<Session, DatabaseError>;
20
+ readonly findById: (
21
+ id: string
22
+ ) => Effect.Effect<Session | undefined, DatabaseError>;
23
+ readonly findByTokenHash: (
24
+ tokenHash: string
25
+ ) => Effect.Effect<Session | undefined, DatabaseError>;
26
+ readonly updateTokenHash: (
27
+ id: string,
28
+ tokenHash: string
29
+ ) => Effect.Effect<Session, DatabaseError>;
30
+ readonly revoke: (id: string) => Effect.Effect<void, DatabaseError>;
31
+ readonly revokeAllForUser: (
32
+ userId: string
33
+ ) => Effect.Effect<void, DatabaseError>;
34
+ readonly deleteExpired: () => Effect.Effect<number, DatabaseError>;
35
+ }
36
+ >() {
37
+ static Live = Layer.effect(
38
+ this,
39
+ Effect.gen(function* (_) {
40
+ const db = yield* _(DatabaseService);
41
+
42
+ const create = (
43
+ data: CreateSessionData
44
+ ): Effect.Effect<Session, DatabaseError> =>
45
+ Effect.gen(function* (_) {
46
+ const id = randomUUID();
47
+ const now = Date.now();
48
+
49
+ yield* _(
50
+ db.run(
51
+ `INSERT INTO sessions (
52
+ id, user_id, token_hash, ip_address, user_agent,
53
+ created_at, expires_at, is_revoked, revoked_at
54
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
55
+ [
56
+ id,
57
+ data.userId,
58
+ data.tokenHash,
59
+ data.ipAddress,
60
+ data.userAgent,
61
+ now,
62
+ data.expiresAt,
63
+ 0, // is_revoked = false
64
+ null, // revoked_at = null
65
+ ]
66
+ )
67
+ );
68
+
69
+ const session = yield* _(findById(id));
70
+ if (!session) {
71
+ return yield* _(
72
+ Effect.fail(
73
+ new DatabaseError("Failed to retrieve created session")
74
+ )
75
+ );
76
+ }
77
+
78
+ return session;
79
+ });
80
+
81
+ const findById = (
82
+ id: string
83
+ ): Effect.Effect<Session | undefined, DatabaseError> =>
84
+ Effect.gen(function* (_) {
85
+ const row = yield* _(
86
+ db.get<{
87
+ id: string;
88
+ user_id: string;
89
+ token_hash: string;
90
+ ip_address: string | null;
91
+ user_agent: string | null;
92
+ created_at: number;
93
+ expires_at: number;
94
+ is_revoked: number;
95
+ revoked_at: number | null;
96
+ }>("SELECT * FROM sessions WHERE id = ?", [id])
97
+ );
98
+
99
+ if (!row) {
100
+ return undefined;
101
+ }
102
+
103
+ return {
104
+ id: row.id,
105
+ userId: row.user_id,
106
+ tokenHash: row.token_hash,
107
+ ipAddress: row.ip_address,
108
+ userAgent: row.user_agent,
109
+ createdAt: row.created_at,
110
+ expiresAt: row.expires_at,
111
+ isRevoked: row.is_revoked === 1,
112
+ revokedAt: row.revoked_at,
113
+ };
114
+ });
115
+
116
+ const findByTokenHash = (
117
+ tokenHash: string
118
+ ): Effect.Effect<Session | undefined, DatabaseError> =>
119
+ Effect.gen(function* (_) {
120
+ const row = yield* _(
121
+ db.get<{
122
+ id: string;
123
+ user_id: string;
124
+ token_hash: string;
125
+ ip_address: string | null;
126
+ user_agent: string | null;
127
+ created_at: number;
128
+ expires_at: number;
129
+ is_revoked: number;
130
+ revoked_at: number | null;
131
+ }>("SELECT * FROM sessions WHERE token_hash = ?", [tokenHash])
132
+ );
133
+
134
+ if (!row) {
135
+ return undefined;
136
+ }
137
+
138
+ return {
139
+ id: row.id,
140
+ userId: row.user_id,
141
+ tokenHash: row.token_hash,
142
+ ipAddress: row.ip_address,
143
+ userAgent: row.user_agent,
144
+ createdAt: row.created_at,
145
+ expiresAt: row.expires_at,
146
+ isRevoked: row.is_revoked === 1,
147
+ revokedAt: row.revoked_at,
148
+ };
149
+ });
150
+
151
+ const updateTokenHash = (
152
+ id: string,
153
+ tokenHash: string
154
+ ): Effect.Effect<Session, DatabaseError> =>
155
+ Effect.gen(function* (_) {
156
+ yield* _(
157
+ db.run(
158
+ `UPDATE sessions
159
+ SET token_hash = ?
160
+ WHERE id = ?`,
161
+ [tokenHash, id]
162
+ )
163
+ );
164
+
165
+ const session = yield* _(findById(id));
166
+ if (!session) {
167
+ return yield* _(
168
+ Effect.fail(
169
+ new DatabaseError("Failed to retrieve updated session")
170
+ )
171
+ );
172
+ }
173
+
174
+ return session;
175
+ });
176
+
177
+ const revoke = (id: string): Effect.Effect<void, DatabaseError> =>
178
+ Effect.gen(function* (_) {
179
+ yield* _(
180
+ db.run(
181
+ `UPDATE sessions
182
+ SET is_revoked = 1, revoked_at = ?
183
+ WHERE id = ?`,
184
+ [Date.now(), id]
185
+ )
186
+ );
187
+ });
188
+
189
+ const revokeAllForUser = (
190
+ userId: string
191
+ ): Effect.Effect<void, DatabaseError> =>
192
+ Effect.gen(function* (_) {
193
+ yield* _(
194
+ db.run(
195
+ `UPDATE sessions
196
+ SET is_revoked = 1, revoked_at = ?
197
+ WHERE user_id = ? AND is_revoked = 0`,
198
+ [Date.now(), userId]
199
+ )
200
+ );
201
+ });
202
+
203
+ const deleteExpired = (): Effect.Effect<number, DatabaseError> =>
204
+ Effect.gen(function* (_) {
205
+ const now = Date.now();
206
+ const result = yield* _(
207
+ db.run<{ changes: number }>(
208
+ `DELETE FROM sessions WHERE expires_at < ?`,
209
+ [now]
210
+ )
211
+ );
212
+
213
+ return result.changes ?? 0;
214
+ });
215
+
216
+ return {
217
+ create,
218
+ findById,
219
+ findByTokenHash,
220
+ updateTokenHash,
221
+ revoke,
222
+ revokeAllForUser,
223
+ deleteExpired,
224
+ };
225
+ })
226
+ );
227
+ }