@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +338 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,327 @@
1
+ import { KoraError } from '@korajs/core'
2
+ import type { AuthUser, StoredUser, UserStore } from '../provider/built-in/user-store'
3
+ import type { Session, SessionStore } from '../session/session'
4
+ import type { AuditAction, AuditLogger } from './audit-log'
5
+
6
+ // ============================================================================
7
+ // Admin API Types
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Configuration for the Admin API.
12
+ */
13
+ export interface AdminApiConfig {
14
+ /** User store for managing users */
15
+ userStore: UserStore
16
+ /** Session store for managing sessions (optional) */
17
+ sessionStore?: SessionStore
18
+ /** Audit logger (optional) */
19
+ auditLogger?: AuditLogger
20
+ /**
21
+ * Authorization check for the acting admin. When set, every method that
22
+ * takes an `adminId` throws {@link AdminUnauthorizedError} unless it returns
23
+ * true. Without it, the app must gate access to AdminApi itself.
24
+ */
25
+ isAdmin?: (adminId: string) => boolean | Promise<boolean>
26
+ /**
27
+ * Revoke every credential of a user (defaults to the user store's token
28
+ * revocation store). Called by `revokeUserSessions`, `suspend`-style flows
29
+ * and `deleteUser`, so JWTs and refresh tokens die with the session rows.
30
+ * Pass `authServer.revokeAllForUser` to also end live sync sessions.
31
+ */
32
+ revokeAllForUser?: (userId: string) => Promise<void>
33
+ }
34
+
35
+ /**
36
+ * Paginated result set.
37
+ */
38
+ export interface PaginatedResult<T> {
39
+ data: T[]
40
+ total: number
41
+ limit: number
42
+ offset: number
43
+ }
44
+
45
+ /**
46
+ * User list query parameters.
47
+ */
48
+ export interface UserListQuery {
49
+ /** Search by email (substring match) */
50
+ email?: string
51
+ /** Filter by email verified status */
52
+ emailVerified?: boolean
53
+ /** Maximum number of results */
54
+ limit?: number
55
+ /** Offset for pagination */
56
+ offset?: number
57
+ }
58
+
59
+ /**
60
+ * Admin-level user update (different from self-update).
61
+ */
62
+ export interface AdminUserUpdate {
63
+ /** Update display name */
64
+ name?: string
65
+ /** Set email verified status */
66
+ emailVerified?: boolean
67
+ /** Update email (admin override) */
68
+ email?: string
69
+ }
70
+
71
+ // ============================================================================
72
+ // Errors
73
+ // ============================================================================
74
+
75
+ export class AdminApiError extends KoraError {
76
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
77
+ super(message, code, context)
78
+ this.name = 'AdminApiError'
79
+ }
80
+ }
81
+
82
+ export class AdminUserNotFoundError extends AdminApiError {
83
+ constructor(userId: string) {
84
+ super(`User "${userId}" not found.`, 'ADMIN_USER_NOT_FOUND', { userId })
85
+ }
86
+ }
87
+
88
+ export class AdminUnauthorizedError extends AdminApiError {
89
+ constructor() {
90
+ super('Admin privileges required.', 'ADMIN_UNAUTHORIZED')
91
+ }
92
+ }
93
+
94
+ // ============================================================================
95
+ // AdminApi
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Administrative API for managing users, sessions, and system configuration.
100
+ *
101
+ * Provides elevated operations that should only be accessible to administrators.
102
+ * All operations are audit-logged when an AuditLogger is configured.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const admin = new AdminApi({
107
+ * userStore: myUserStore,
108
+ * sessionStore: mySessionStore,
109
+ * auditLogger: myAuditLogger,
110
+ * })
111
+ *
112
+ * // List users
113
+ * const { data, total } = await admin.listUsers({ limit: 20 })
114
+ *
115
+ * // Suspend a user (revokes all sessions)
116
+ * await admin.suspendUser('admin-user-id', 'target-user-id', 'Policy violation')
117
+ * ```
118
+ */
119
+ export class AdminApi {
120
+ private readonly userStore: UserStore
121
+ private readonly sessionStore: SessionStore | null
122
+ private readonly auditLogger: AuditLogger | null
123
+ private readonly isAdmin: AdminApiConfig['isAdmin']
124
+ private readonly revokeAllForUserFn: AdminApiConfig['revokeAllForUser']
125
+
126
+ constructor(config: AdminApiConfig) {
127
+ this.userStore = config.userStore
128
+ this.sessionStore = config.sessionStore ?? null
129
+ this.auditLogger = config.auditLogger ?? null
130
+ this.isAdmin = config.isAdmin
131
+ this.revokeAllForUserFn = config.revokeAllForUser
132
+ }
133
+
134
+ /** Throws AdminUnauthorizedError unless the configured `isAdmin` accepts the actor. */
135
+ private async authorize(adminId: string): Promise<void> {
136
+ if (this.isAdmin && !(await this.isAdmin(adminId))) {
137
+ throw new AdminUnauthorizedError()
138
+ }
139
+ }
140
+
141
+ /** Kill every JWT and refresh token of a user, not just session rows (AUTH-7b/AUTH-14). */
142
+ private async revokeCredentials(userId: string): Promise<void> {
143
+ if (this.revokeAllForUserFn) {
144
+ await this.revokeAllForUserFn(userId)
145
+ return
146
+ }
147
+ await this.userStore.getTokenRevocationStore?.()?.revokeAllForUser(userId, Date.now())
148
+ }
149
+
150
+ /**
151
+ * Get a user by ID with full details.
152
+ */
153
+ async getUser(adminId: string, userId: string): Promise<AuthUser> {
154
+ await this.authorize(adminId)
155
+ const user = await this.userStore.findById(userId)
156
+ if (!user) {
157
+ throw new AdminUserNotFoundError(userId)
158
+ }
159
+
160
+ await this.audit('admin.user_lookup', adminId, userId, 'user')
161
+
162
+ return toAuthUser(user)
163
+ }
164
+
165
+ /**
166
+ * List users with optional filtering and pagination.
167
+ */
168
+ async listUsers(query: UserListQuery = {}): Promise<PaginatedResult<AuthUser>> {
169
+ const limit = query.limit ?? 50
170
+ const offset = query.offset ?? 0
171
+
172
+ // Get all users (InMemoryUserStore doesn't have a list method, so we use findByEmail with patterns)
173
+ // For now, we expose a listing helper
174
+ const allUsers = await this.userStore.listAll()
175
+
176
+ let filtered = allUsers
177
+
178
+ if (query.email) {
179
+ const searchEmail = query.email.toLowerCase()
180
+ filtered = filtered.filter((u) => u.email.toLowerCase().includes(searchEmail))
181
+ }
182
+
183
+ if (query.emailVerified !== undefined) {
184
+ filtered = filtered.filter((u) => u.emailVerified === query.emailVerified)
185
+ }
186
+
187
+ const total = filtered.length
188
+ const data = filtered.slice(offset, offset + limit).map(toAuthUser)
189
+
190
+ return { data, total, limit, offset }
191
+ }
192
+
193
+ /**
194
+ * Update a user's profile (admin-level).
195
+ */
196
+ async updateUser(adminId: string, userId: string, updates: AdminUserUpdate): Promise<AuthUser> {
197
+ await this.authorize(adminId)
198
+ const user = await this.userStore.findById(userId)
199
+ if (!user) {
200
+ throw new AdminUserNotFoundError(userId)
201
+ }
202
+
203
+ if (updates.name !== undefined) {
204
+ user.name = updates.name
205
+ }
206
+ if (updates.emailVerified !== undefined) {
207
+ user.emailVerified = updates.emailVerified
208
+ }
209
+ if (updates.email !== undefined) {
210
+ user.email = updates.email.toLowerCase().trim()
211
+ }
212
+
213
+ await this.userStore.update(user)
214
+
215
+ await this.audit('user.update', adminId, userId, 'user', { updates })
216
+
217
+ return toAuthUser(user)
218
+ }
219
+
220
+ /**
221
+ * Delete a user and all associated sessions.
222
+ */
223
+ async deleteUser(adminId: string, userId: string): Promise<void> {
224
+ await this.authorize(adminId)
225
+ const user = await this.userStore.findById(userId)
226
+ if (!user) {
227
+ throw new AdminUserNotFoundError(userId)
228
+ }
229
+
230
+ // Revoke all sessions and credentials first
231
+ if (this.sessionStore) {
232
+ await this.sessionStore.deleteAllForUser(userId)
233
+ }
234
+ await this.revokeCredentials(userId)
235
+
236
+ await this.userStore.delete(userId)
237
+
238
+ await this.audit('user.delete', adminId, userId, 'user')
239
+ }
240
+
241
+ /**
242
+ * Get all active sessions for a user.
243
+ */
244
+ async getUserSessions(userId: string): Promise<Session[]> {
245
+ if (!this.sessionStore) return []
246
+ return this.sessionStore.listByUserId(userId)
247
+ }
248
+
249
+ /**
250
+ * Revoke all sessions for a user.
251
+ */
252
+ async revokeUserSessions(adminId: string, userId: string): Promise<number> {
253
+ await this.authorize(adminId)
254
+ await this.revokeCredentials(userId)
255
+ if (!this.sessionStore) return 0
256
+
257
+ const count = await this.sessionStore.deleteAllForUser(userId)
258
+
259
+ await this.audit('session.revoke_all', adminId, userId, 'user', { sessionsRevoked: count })
260
+
261
+ return count
262
+ }
263
+
264
+ /**
265
+ * Revoke a specific session.
266
+ */
267
+ async revokeSession(adminId: string, sessionId: string): Promise<void> {
268
+ await this.authorize(adminId)
269
+ if (!this.sessionStore) return
270
+
271
+ await this.sessionStore.delete(sessionId)
272
+
273
+ await this.audit('session.revoke', adminId, sessionId, 'session')
274
+ }
275
+
276
+ /**
277
+ * Get system statistics.
278
+ */
279
+ async getStats(): Promise<{
280
+ totalUsers: number
281
+ verifiedUsers: number
282
+ unverifiedUsers: number
283
+ }> {
284
+ const allUsers = await this.userStore.listAll()
285
+ const verified = allUsers.filter((u) => u.emailVerified).length
286
+
287
+ return {
288
+ totalUsers: allUsers.length,
289
+ verifiedUsers: verified,
290
+ unverifiedUsers: allUsers.length - verified,
291
+ }
292
+ }
293
+
294
+ // --- Private ---
295
+
296
+ private async audit(
297
+ action: AuditAction,
298
+ actorId: string,
299
+ targetId: string,
300
+ targetType: string,
301
+ metadata?: Record<string, unknown>,
302
+ ): Promise<void> {
303
+ if (!this.auditLogger) return
304
+ await this.auditLogger.log({
305
+ action,
306
+ actorId,
307
+ actorType: 'admin',
308
+ targetId,
309
+ targetType,
310
+ metadata,
311
+ })
312
+ }
313
+ }
314
+
315
+ // ============================================================================
316
+ // Helpers
317
+ // ============================================================================
318
+
319
+ function toAuthUser(stored: StoredUser): AuthUser {
320
+ return {
321
+ id: stored.id,
322
+ email: stored.email,
323
+ name: stored.name,
324
+ emailVerified: stored.emailVerified,
325
+ createdAt: stored.createdAt,
326
+ }
327
+ }
@@ -0,0 +1,324 @@
1
+ import { KoraError } from '@korajs/core'
2
+
3
+ // ============================================================================
4
+ // Audit Log Types
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Actions that can be audited.
9
+ */
10
+ const AUDIT_ACTIONS = [
11
+ // Auth
12
+ 'user.signup',
13
+ 'user.signin',
14
+ 'user.signout',
15
+ 'user.token_refresh',
16
+ 'user.password_change',
17
+ 'user.password_reset_request',
18
+ 'user.password_reset',
19
+ 'user.email_verify',
20
+ // MFA
21
+ 'mfa.enable',
22
+ 'mfa.verify_setup',
23
+ 'mfa.verify',
24
+ 'mfa.recovery_used',
25
+ 'mfa.recovery_regenerate',
26
+ 'mfa.disable',
27
+ // Session
28
+ 'session.create',
29
+ 'session.revoke',
30
+ 'session.revoke_all',
31
+ 'session.expired',
32
+ // OAuth
33
+ 'oauth.authorize',
34
+ 'oauth.callback',
35
+ 'oauth.link',
36
+ 'oauth.unlink',
37
+ // User management
38
+ 'user.create',
39
+ 'user.update',
40
+ 'user.delete',
41
+ 'user.suspend',
42
+ 'user.unsuspend',
43
+ // Org management
44
+ 'org.create',
45
+ 'org.update',
46
+ 'org.delete',
47
+ 'org.member_add',
48
+ 'org.member_remove',
49
+ 'org.member_role_change',
50
+ 'org.ownership_transfer',
51
+ 'org.invitation_create',
52
+ 'org.invitation_accept',
53
+ 'org.invitation_revoke',
54
+ // Admin
55
+ 'admin.user_lookup',
56
+ 'admin.impersonate',
57
+ 'admin.config_change',
58
+ ] as const
59
+
60
+ export type AuditAction = (typeof AUDIT_ACTIONS)[number]
61
+
62
+ /**
63
+ * An audit log entry.
64
+ */
65
+ export interface AuditEntry {
66
+ /** Unique entry ID */
67
+ id: string
68
+ /** When this event occurred */
69
+ timestamp: number
70
+ /** The action that was performed */
71
+ action: AuditAction
72
+ /** Who performed the action (user ID, "system", or "anonymous") */
73
+ actorId: string
74
+ /** The type of actor */
75
+ actorType: 'user' | 'admin' | 'system'
76
+ /** The target of the action (e.g., user ID, org ID, session ID) */
77
+ targetId: string | null
78
+ /** The type of target */
79
+ targetType: string | null
80
+ /** IP address of the actor */
81
+ ipAddress: string | null
82
+ /** User agent */
83
+ userAgent: string | null
84
+ /** Whether the action succeeded */
85
+ success: boolean
86
+ /** Error message if the action failed */
87
+ errorMessage: string | null
88
+ /** Additional structured context */
89
+ metadata?: Record<string, unknown>
90
+ }
91
+
92
+ /**
93
+ * Query parameters for searching audit logs.
94
+ */
95
+ export interface AuditLogQuery {
96
+ /** Filter by actor ID */
97
+ actorId?: string
98
+ /** Filter by target ID */
99
+ targetId?: string
100
+ /** Filter by action(s) */
101
+ actions?: AuditAction[]
102
+ /** Filter by success/failure */
103
+ success?: boolean
104
+ /** Start time (inclusive) */
105
+ startTime?: number
106
+ /** End time (inclusive) */
107
+ endTime?: number
108
+ /** Maximum number of entries to return */
109
+ limit?: number
110
+ /** Offset for pagination */
111
+ offset?: number
112
+ }
113
+
114
+ /**
115
+ * Store for audit log entries.
116
+ */
117
+ export interface AuditLogStore {
118
+ /** Append an audit entry */
119
+ append(entry: AuditEntry): Promise<void>
120
+ /** Query audit entries */
121
+ query(params: AuditLogQuery): Promise<AuditEntry[]>
122
+ /** Count matching entries */
123
+ count(params: AuditLogQuery): Promise<number>
124
+ /** Delete entries older than a given timestamp */
125
+ purgeOlderThan(timestamp: number): Promise<number>
126
+ }
127
+
128
+ // ============================================================================
129
+ // Errors
130
+ // ============================================================================
131
+
132
+ export class AuditLogError extends KoraError {
133
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
134
+ super(message, code, context)
135
+ this.name = 'AuditLogError'
136
+ }
137
+ }
138
+
139
+ // ============================================================================
140
+ // InMemoryAuditLogStore
141
+ // ============================================================================
142
+
143
+ /**
144
+ * In-memory audit log store for development and testing.
145
+ */
146
+ export class InMemoryAuditLogStore implements AuditLogStore {
147
+ private readonly entries: AuditEntry[] = []
148
+
149
+ async append(entry: AuditEntry): Promise<void> {
150
+ this.entries.push({ ...entry })
151
+ }
152
+
153
+ async query(params: AuditLogQuery): Promise<AuditEntry[]> {
154
+ let results = this.filterEntries(params)
155
+
156
+ // Sort by timestamp descending (newest first)
157
+ results.sort((a, b) => b.timestamp - a.timestamp)
158
+
159
+ // Pagination
160
+ const offset = params.offset ?? 0
161
+ const limit = params.limit ?? 100
162
+ results = results.slice(offset, offset + limit)
163
+
164
+ return results.map((e) => ({ ...e }))
165
+ }
166
+
167
+ async count(params: AuditLogQuery): Promise<number> {
168
+ return this.filterEntries(params).length
169
+ }
170
+
171
+ async purgeOlderThan(timestamp: number): Promise<number> {
172
+ const initialLength = this.entries.length
173
+ let writeIndex = 0
174
+ for (let i = 0; i < this.entries.length; i++) {
175
+ const entry = this.entries[i] as AuditEntry
176
+ if (entry.timestamp >= timestamp) {
177
+ this.entries[writeIndex] = entry
178
+ writeIndex++
179
+ }
180
+ }
181
+ this.entries.length = writeIndex
182
+ return initialLength - writeIndex
183
+ }
184
+
185
+ private filterEntries(params: AuditLogQuery): AuditEntry[] {
186
+ return this.entries.filter((e) => {
187
+ if (params.actorId !== undefined && e.actorId !== params.actorId) return false
188
+ if (params.targetId !== undefined && e.targetId !== params.targetId) return false
189
+ if (params.actions !== undefined && !params.actions.includes(e.action)) return false
190
+ if (params.success !== undefined && e.success !== params.success) return false
191
+ if (params.startTime !== undefined && e.timestamp < params.startTime) return false
192
+ if (params.endTime !== undefined && e.timestamp > params.endTime) return false
193
+ return true
194
+ })
195
+ }
196
+ }
197
+
198
+ // ============================================================================
199
+ // AuditLogger
200
+ // ============================================================================
201
+
202
+ /**
203
+ * Structured audit logger for authentication events.
204
+ *
205
+ * Records all security-relevant actions for compliance and debugging.
206
+ * Designed to be plugged into the auth system at key decision points.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const auditLog = new AuditLogger({
211
+ * store: new InMemoryAuditLogStore(),
212
+ * })
213
+ *
214
+ * await auditLog.log({
215
+ * action: 'user.signin',
216
+ * actorId: 'user-123',
217
+ * actorType: 'user',
218
+ * success: true,
219
+ * ipAddress: '192.168.1.1',
220
+ * })
221
+ *
222
+ * const entries = await auditLog.query({ actorId: 'user-123', limit: 50 })
223
+ * ```
224
+ */
225
+ export class AuditLogger {
226
+ private readonly store: AuditLogStore
227
+ private readonly retentionMs: number | null
228
+
229
+ constructor(config: { store: AuditLogStore; retentionDays?: number }) {
230
+ this.store = config.store
231
+ this.retentionMs = config.retentionDays ? config.retentionDays * 24 * 60 * 60 * 1000 : null
232
+ }
233
+
234
+ /**
235
+ * Log an audit event.
236
+ */
237
+ async log(params: {
238
+ action: AuditAction
239
+ actorId: string
240
+ actorType?: 'user' | 'admin' | 'system'
241
+ targetId?: string
242
+ targetType?: string
243
+ ipAddress?: string
244
+ userAgent?: string
245
+ success?: boolean
246
+ errorMessage?: string
247
+ metadata?: Record<string, unknown>
248
+ }): Promise<AuditEntry> {
249
+ const entry: AuditEntry = {
250
+ id: generateAuditId(),
251
+ timestamp: Date.now(),
252
+ action: params.action,
253
+ actorId: params.actorId,
254
+ actorType: params.actorType ?? 'user',
255
+ targetId: params.targetId ?? null,
256
+ targetType: params.targetType ?? null,
257
+ ipAddress: params.ipAddress ?? null,
258
+ userAgent: params.userAgent ?? null,
259
+ success: params.success ?? true,
260
+ errorMessage: params.errorMessage ?? null,
261
+ metadata: params.metadata,
262
+ }
263
+
264
+ await this.store.append(entry)
265
+ return entry
266
+ }
267
+
268
+ /**
269
+ * Query audit log entries.
270
+ */
271
+ async query(params: AuditLogQuery): Promise<AuditEntry[]> {
272
+ return this.store.query(params)
273
+ }
274
+
275
+ /**
276
+ * Count matching audit entries.
277
+ */
278
+ async count(params: AuditLogQuery): Promise<number> {
279
+ return this.store.count(params)
280
+ }
281
+
282
+ /**
283
+ * Purge old entries based on retention policy.
284
+ * Returns the number of entries purged.
285
+ */
286
+ async purge(): Promise<number> {
287
+ if (this.retentionMs === null) return 0
288
+ const cutoff = Date.now() - this.retentionMs
289
+ return this.store.purgeOlderThan(cutoff)
290
+ }
291
+
292
+ /**
293
+ * Get recent activity for a user.
294
+ */
295
+ async getUserActivity(userId: string, limit = 50): Promise<AuditEntry[]> {
296
+ return this.store.query({ actorId: userId, limit })
297
+ }
298
+
299
+ /**
300
+ * Get failed login attempts for a user within a time window.
301
+ */
302
+ async getFailedLogins(userId: string, windowMs: number): Promise<AuditEntry[]> {
303
+ return this.store.query({
304
+ targetId: userId,
305
+ actions: ['user.signin'],
306
+ success: false,
307
+ startTime: Date.now() - windowMs,
308
+ })
309
+ }
310
+ }
311
+
312
+ // ============================================================================
313
+ // Helpers
314
+ // ============================================================================
315
+
316
+ function generateAuditId(): string {
317
+ const bytes = new Uint8Array(16)
318
+ globalThis.crypto.getRandomValues(bytes)
319
+ let hex = ''
320
+ for (let i = 0; i < bytes.length; i++) {
321
+ hex += bytes[i]?.toString(16).padStart(2, '0')
322
+ }
323
+ return hex
324
+ }