convex-audit-log 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 (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +408 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +336 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +297 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +36 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +317 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/convex.config.d.ts +3 -0
  28. package/dist/component/convex.config.d.ts.map +1 -0
  29. package/dist/component/convex.config.js +3 -0
  30. package/dist/component/convex.config.js.map +1 -0
  31. package/dist/component/lib.d.ts +341 -0
  32. package/dist/component/lib.d.ts.map +1 -0
  33. package/dist/component/lib.js +598 -0
  34. package/dist/component/lib.js.map +1 -0
  35. package/dist/component/schema.d.ts +87 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +71 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/dist/component/shared.d.ts +203 -0
  40. package/dist/component/shared.d.ts.map +1 -0
  41. package/dist/component/shared.js +94 -0
  42. package/dist/component/shared.js.map +1 -0
  43. package/dist/react/index.d.ts +247 -0
  44. package/dist/react/index.d.ts.map +1 -0
  45. package/dist/react/index.js +196 -0
  46. package/dist/react/index.js.map +1 -0
  47. package/package.json +115 -0
  48. package/src/client/_generated/_ignore.ts +1 -0
  49. package/src/client/index.test.ts +61 -0
  50. package/src/client/index.ts +525 -0
  51. package/src/client/setup.test.ts +26 -0
  52. package/src/component/_generated/api.ts +52 -0
  53. package/src/component/_generated/component.ts +392 -0
  54. package/src/component/_generated/dataModel.ts +60 -0
  55. package/src/component/_generated/server.ts +161 -0
  56. package/src/component/convex.config.ts +3 -0
  57. package/src/component/lib.test.ts +171 -0
  58. package/src/component/lib.ts +722 -0
  59. package/src/component/schema.ts +93 -0
  60. package/src/component/setup.test.ts +11 -0
  61. package/src/component/shared.ts +167 -0
  62. package/src/react/index.ts +305 -0
  63. package/src/test.ts +18 -0
@@ -0,0 +1,93 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ /**
5
+ * Severity levels for audit events.
6
+ * - info: Regular operations (login, profile update)
7
+ * - warning: Potentially concerning actions (password change, permission change)
8
+ * - error: Failed operations or errors
9
+ * - critical: Security-sensitive events (unauthorized access attempts, data breaches)
10
+ */
11
+ export const vSeverity = v.union(
12
+ v.literal("info"),
13
+ v.literal("warning"),
14
+ v.literal("error"),
15
+ v.literal("critical")
16
+ );
17
+
18
+ export type Severity = "info" | "warning" | "error" | "critical";
19
+
20
+ export default defineSchema({
21
+ /**
22
+ * Main audit log table storing all audit events.
23
+ */
24
+ auditLogs: defineTable({
25
+ // Core event data
26
+ action: v.string(),
27
+ actorId: v.optional(v.string()),
28
+ timestamp: v.number(),
29
+
30
+ // Resource tracking
31
+ resourceType: v.optional(v.string()),
32
+ resourceId: v.optional(v.string()),
33
+
34
+ // Event metadata
35
+ metadata: v.optional(v.any()),
36
+ severity: vSeverity,
37
+
38
+ // Context information
39
+ ipAddress: v.optional(v.string()),
40
+ userAgent: v.optional(v.string()),
41
+ sessionId: v.optional(v.string()),
42
+
43
+ // Compliance tags
44
+ tags: v.optional(v.array(v.string())),
45
+
46
+ // Change tracking
47
+ before: v.optional(v.any()),
48
+ after: v.optional(v.any()),
49
+ diff: v.optional(v.string()),
50
+
51
+ // Retention category for cleanup policies
52
+ retentionCategory: v.optional(v.string()),
53
+ })
54
+ // Query by action type with time ordering
55
+ .index("by_action_timestamp", ["action", "timestamp"])
56
+ // Query by actor (user) with time ordering
57
+ .index("by_actor_timestamp", ["actorId", "timestamp"])
58
+ // Query by resource with time ordering
59
+ .index("by_resource", ["resourceType", "resourceId", "timestamp"])
60
+ // Query by severity level with time ordering
61
+ .index("by_severity_timestamp", ["severity", "timestamp"])
62
+ // Query by timestamp for time-range queries
63
+ .index("by_timestamp", ["timestamp"])
64
+ // Query by retention category for cleanup
65
+ .index("by_retention_timestamp", ["retentionCategory", "timestamp"]),
66
+
67
+ /**
68
+ * Configuration table for component settings.
69
+ * Contains a single document with global configuration.
70
+ */
71
+ config: defineTable({
72
+ // Retention settings
73
+ defaultRetentionDays: v.number(),
74
+ criticalRetentionDays: v.number(),
75
+
76
+ // PII redaction settings
77
+ piiFieldsToRedact: v.array(v.string()),
78
+
79
+ // Sampling settings (for high-volume apps)
80
+ samplingEnabled: v.boolean(),
81
+ samplingRate: v.number(), // 0.0 to 1.0
82
+
83
+ // Custom retention categories
84
+ customRetention: v.optional(
85
+ v.array(
86
+ v.object({
87
+ category: v.string(),
88
+ retentionDays: v.number(),
89
+ })
90
+ )
91
+ ),
92
+ }),
93
+ });
@@ -0,0 +1,11 @@
1
+ /// <reference types="vite/client" />
2
+ import { test } from "vitest";
3
+ import schema from "./schema.js";
4
+ import { convexTest } from "convex-test";
5
+ export const modules = import.meta.glob("./**/*.*s");
6
+
7
+ export function initConvexTest() {
8
+ const t = convexTest(schema, modules);
9
+ return t;
10
+ }
11
+ test("setup", () => {});
@@ -0,0 +1,167 @@
1
+ import { v } from "convex/values";
2
+ import type { Infer } from "convex/values";
3
+
4
+ /**
5
+ * Severity levels for audit events.
6
+ */
7
+ export const vSeverity = v.union(
8
+ v.literal("info"),
9
+ v.literal("warning"),
10
+ v.literal("error"),
11
+ v.literal("critical")
12
+ );
13
+
14
+ /**
15
+ * Base audit event validator (without timestamp - added automatically).
16
+ */
17
+ export const vAuditEventInput = v.object({
18
+ action: v.string(),
19
+ actorId: v.optional(v.string()),
20
+ resourceType: v.optional(v.string()),
21
+ resourceId: v.optional(v.string()),
22
+ metadata: v.optional(v.any()),
23
+ severity: vSeverity,
24
+ ipAddress: v.optional(v.string()),
25
+ userAgent: v.optional(v.string()),
26
+ sessionId: v.optional(v.string()),
27
+ tags: v.optional(v.array(v.string())),
28
+ retentionCategory: v.optional(v.string()),
29
+ });
30
+
31
+ /**
32
+ * Change tracking event validator.
33
+ */
34
+ export const vChangeEventInput = v.object({
35
+ action: v.string(),
36
+ actorId: v.optional(v.string()),
37
+ resourceType: v.string(),
38
+ resourceId: v.string(),
39
+ before: v.optional(v.any()),
40
+ after: v.optional(v.any()),
41
+ generateDiff: v.optional(v.boolean()),
42
+ severity: v.optional(vSeverity),
43
+ ipAddress: v.optional(v.string()),
44
+ userAgent: v.optional(v.string()),
45
+ sessionId: v.optional(v.string()),
46
+ tags: v.optional(v.array(v.string())),
47
+ retentionCategory: v.optional(v.string()),
48
+ });
49
+
50
+ /**
51
+ * Query filters validator.
52
+ */
53
+ export const vQueryFilters = v.object({
54
+ severity: v.optional(v.array(vSeverity)),
55
+ actions: v.optional(v.array(v.string())),
56
+ resourceTypes: v.optional(v.array(v.string())),
57
+ actorIds: v.optional(v.array(v.string())),
58
+ fromTimestamp: v.optional(v.number()),
59
+ toTimestamp: v.optional(v.number()),
60
+ tags: v.optional(v.array(v.string())),
61
+ });
62
+
63
+ /**
64
+ * Pagination validator.
65
+ */
66
+ export const vPagination = v.object({
67
+ cursor: v.optional(v.string()),
68
+ limit: v.number(),
69
+ });
70
+
71
+ /**
72
+ * Report format validator.
73
+ */
74
+ export const vReportFormat = v.union(
75
+ v.literal("json"),
76
+ v.literal("csv")
77
+ );
78
+
79
+ /**
80
+ * Anomaly pattern validator.
81
+ */
82
+ export const vAnomalyPattern = v.object({
83
+ action: v.string(),
84
+ threshold: v.number(),
85
+ windowMinutes: v.number(),
86
+ });
87
+
88
+ /**
89
+ * Cleanup options validator.
90
+ */
91
+ export const vCleanupOptions = v.object({
92
+ olderThanDays: v.optional(v.number()),
93
+ preserveSeverity: v.optional(v.array(vSeverity)),
94
+ retentionCategory: v.optional(v.string()),
95
+ batchSize: v.optional(v.number()),
96
+ });
97
+
98
+ /**
99
+ * Configuration options validator.
100
+ */
101
+ export const vConfigOptions = v.object({
102
+ defaultRetentionDays: v.optional(v.number()),
103
+ criticalRetentionDays: v.optional(v.number()),
104
+ piiFieldsToRedact: v.optional(v.array(v.string())),
105
+ samplingEnabled: v.optional(v.boolean()),
106
+ samplingRate: v.optional(v.number()),
107
+ customRetention: v.optional(
108
+ v.array(
109
+ v.object({
110
+ category: v.string(),
111
+ retentionDays: v.number(),
112
+ })
113
+ )
114
+ ),
115
+ });
116
+
117
+ export type Severity = Infer<typeof vSeverity>;
118
+ export type AuditEventInput = Infer<typeof vAuditEventInput>;
119
+ export type ChangeEventInput = Infer<typeof vChangeEventInput>;
120
+ export type QueryFilters = Infer<typeof vQueryFilters>;
121
+ export type Pagination = Infer<typeof vPagination>;
122
+ export type ReportFormat = Infer<typeof vReportFormat>;
123
+ export type AnomalyPattern = Infer<typeof vAnomalyPattern>;
124
+ export type CleanupOptions = Infer<typeof vCleanupOptions>;
125
+ export type ConfigOptions = Infer<typeof vConfigOptions>;
126
+
127
+ /**
128
+ * Full audit event with generated fields.
129
+ */
130
+ export type AuditEvent = AuditEventInput & {
131
+ _id: string;
132
+ _creationTime: number;
133
+ timestamp: number;
134
+ before?: unknown;
135
+ after?: unknown;
136
+ diff?: string;
137
+ };
138
+
139
+ /**
140
+ * Paginated result type.
141
+ */
142
+ export type PaginatedResult<T> = {
143
+ items: T[];
144
+ cursor: string | null;
145
+ hasMore: boolean;
146
+ };
147
+
148
+ /**
149
+ * Anomaly detection result.
150
+ */
151
+ export type Anomaly = {
152
+ action: string;
153
+ count: number;
154
+ threshold: number;
155
+ windowMinutes: number;
156
+ detectedAt: number;
157
+ };
158
+
159
+ /**
160
+ * Report result type.
161
+ */
162
+ export type Report = {
163
+ format: ReportFormat;
164
+ data: string;
165
+ generatedAt: number;
166
+ recordCount: number;
167
+ };
@@ -0,0 +1,305 @@
1
+ "use client";
2
+
3
+ import { useQuery } from "convex/react";
4
+ import type { FunctionReference } from "convex/server";
5
+
6
+ export type Severity = "info" | "warning" | "error" | "critical";
7
+
8
+ export interface AuditLogEntry {
9
+ _id: string;
10
+ _creationTime: number;
11
+ action: string;
12
+ actorId?: string;
13
+ timestamp: number;
14
+ resourceType?: string;
15
+ resourceId?: string;
16
+ metadata?: unknown;
17
+ severity: Severity;
18
+ ipAddress?: string;
19
+ userAgent?: string;
20
+ sessionId?: string;
21
+ tags?: string[];
22
+ before?: unknown;
23
+ after?: unknown;
24
+ diff?: string;
25
+ retentionCategory?: string;
26
+ }
27
+
28
+ export interface AuditLogStats {
29
+ totalCount: number;
30
+ bySeverity: {
31
+ info: number;
32
+ warning: number;
33
+ error: number;
34
+ critical: number;
35
+ };
36
+ topActions: { action: string; count: number }[];
37
+ topActors: { actorId: string; count: number }[];
38
+ }
39
+
40
+ export interface Anomaly {
41
+ action: string;
42
+ count: number;
43
+ threshold: number;
44
+ windowMinutes: number;
45
+ detectedAt: number;
46
+ }
47
+
48
+ /**
49
+ * Hook to query audit logs by resource.
50
+ * Provides real-time updates when the audit log changes.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import { useAuditLogByResource } from "convex-audit-log/react";
55
+ * import { api } from "../convex/_generated/api";
56
+ *
57
+ * function DocumentHistory({ documentId }: { documentId: string }) {
58
+ * const logs = useAuditLogByResource(api.auditLog.queryByResource, {
59
+ * resourceType: "documents",
60
+ * resourceId: documentId,
61
+ * limit: 20,
62
+ * });
63
+ *
64
+ * if (!logs) return <div>Loading...</div>;
65
+ *
66
+ * return (
67
+ * <ul>
68
+ * {logs.map((log) => (
69
+ * <li key={log._id}>
70
+ * {log.action} by {log.actorId} at {new Date(log.timestamp).toLocaleString()}
71
+ * </li>
72
+ * ))}
73
+ * </ul>
74
+ * );
75
+ * }
76
+ * ```
77
+ */
78
+ export function useAuditLogByResource(
79
+ queryRef: FunctionReference<
80
+ "query",
81
+ "public",
82
+ { resourceType: string; resourceId: string; limit?: number; fromTimestamp?: number },
83
+ AuditLogEntry[]
84
+ >,
85
+ args: {
86
+ resourceType: string;
87
+ resourceId: string;
88
+ limit?: number;
89
+ fromTimestamp?: number;
90
+ }
91
+ ): AuditLogEntry[] | undefined {
92
+ return useQuery(queryRef, args);
93
+ }
94
+
95
+ /**
96
+ * Hook to query audit logs by actor (user).
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * import { useAuditLogByActor } from "convex-audit-log/react";
101
+ * import { api } from "../convex/_generated/api";
102
+ *
103
+ * function UserActivity({ userId }: { userId: string }) {
104
+ * const logs = useAuditLogByActor(api.auditLog.queryByActor, {
105
+ * actorId: userId,
106
+ * limit: 50,
107
+ * });
108
+ *
109
+ * if (!logs) return <div>Loading...</div>;
110
+ *
111
+ * return (
112
+ * <ul>
113
+ * {logs.map((log) => (
114
+ * <li key={log._id}>{log.action}</li>
115
+ * ))}
116
+ * </ul>
117
+ * );
118
+ * }
119
+ * ```
120
+ */
121
+ export function useAuditLogByActor(
122
+ queryRef: FunctionReference<
123
+ "query",
124
+ "public",
125
+ { actorId: string; limit?: number; fromTimestamp?: number; actions?: string[] },
126
+ AuditLogEntry[]
127
+ >,
128
+ args: {
129
+ actorId: string;
130
+ limit?: number;
131
+ fromTimestamp?: number;
132
+ actions?: string[];
133
+ }
134
+ ): AuditLogEntry[] | undefined {
135
+ return useQuery(queryRef, args);
136
+ }
137
+
138
+ /**
139
+ * Hook to watch critical events in real-time.
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * import { useWatchCriticalEvents } from "convex-audit-log/react";
144
+ * import { api } from "../convex/_generated/api";
145
+ *
146
+ * function SecurityAlerts() {
147
+ * const criticalEvents = useWatchCriticalEvents(api.auditLog.watchCritical, {
148
+ * limit: 10,
149
+ * });
150
+ *
151
+ * if (!criticalEvents) return <div>Loading...</div>;
152
+ *
153
+ * return (
154
+ * <div className="alerts">
155
+ * {criticalEvents.map((event) => (
156
+ * <div key={event._id} className={`alert alert-${event.severity}`}>
157
+ * {event.action}
158
+ * </div>
159
+ * ))}
160
+ * </div>
161
+ * );
162
+ * }
163
+ * ```
164
+ */
165
+ export function useWatchCriticalEvents(
166
+ queryRef: FunctionReference<
167
+ "query",
168
+ "public",
169
+ { severity?: Severity[]; limit?: number },
170
+ AuditLogEntry[]
171
+ >,
172
+ args?: {
173
+ severity?: Severity[];
174
+ limit?: number;
175
+ }
176
+ ): AuditLogEntry[] | undefined {
177
+ return useQuery(queryRef, args ?? {});
178
+ }
179
+
180
+ /**
181
+ * Hook to get audit log statistics.
182
+ *
183
+ * @example
184
+ * ```tsx
185
+ * import { useAuditLogStats } from "convex-audit-log/react";
186
+ * import { api } from "../convex/_generated/api";
187
+ *
188
+ * function Dashboard() {
189
+ * const stats = useAuditLogStats(api.auditLog.getStats, {
190
+ * fromTimestamp: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
191
+ * });
192
+ *
193
+ * if (!stats) return <div>Loading...</div>;
194
+ *
195
+ * return (
196
+ * <div>
197
+ * <h2>Total Events: {stats.totalCount}</h2>
198
+ * <div>Critical: {stats.bySeverity.critical}</div>
199
+ * <div>Errors: {stats.bySeverity.error}</div>
200
+ * </div>
201
+ * );
202
+ * }
203
+ * ```
204
+ */
205
+ export function useAuditLogStats(
206
+ queryRef: FunctionReference<
207
+ "query",
208
+ "public",
209
+ { fromTimestamp?: number; toTimestamp?: number },
210
+ AuditLogStats
211
+ >,
212
+ args?: {
213
+ fromTimestamp?: number;
214
+ toTimestamp?: number;
215
+ }
216
+ ): AuditLogStats | undefined {
217
+ return useQuery(queryRef, args ?? {});
218
+ }
219
+
220
+ /**
221
+ * Hook to detect anomalies in real-time.
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * import { useAnomalyDetection } from "convex-audit-log/react";
226
+ * import { api } from "../convex/_generated/api";
227
+ *
228
+ * function AnomalyMonitor() {
229
+ * const anomalies = useAnomalyDetection(api.auditLog.detectAnomalies, {
230
+ * patterns: [
231
+ * { action: "user.login.failed", threshold: 5, windowMinutes: 5 },
232
+ * { action: "record.deleted", threshold: 10, windowMinutes: 1 },
233
+ * ],
234
+ * });
235
+ *
236
+ * if (!anomalies) return <div>Loading...</div>;
237
+ * if (anomalies.length === 0) return <div>No anomalies detected</div>;
238
+ *
239
+ * return (
240
+ * <div className="anomalies">
241
+ * {anomalies.map((anomaly, i) => (
242
+ * <div key={i} className="anomaly-alert">
243
+ * {anomaly.action}: {anomaly.count} events in {anomaly.windowMinutes} minutes
244
+ * (threshold: {anomaly.threshold})
245
+ * </div>
246
+ * ))}
247
+ * </div>
248
+ * );
249
+ * }
250
+ * ```
251
+ */
252
+ export function useAnomalyDetection(
253
+ queryRef: FunctionReference<
254
+ "query",
255
+ "public",
256
+ { patterns: { action: string; threshold: number; windowMinutes: number }[] },
257
+ Anomaly[]
258
+ >,
259
+ args: {
260
+ patterns: { action: string; threshold: number; windowMinutes: number }[];
261
+ }
262
+ ): Anomaly[] | undefined {
263
+ return useQuery(queryRef, args);
264
+ }
265
+
266
+ /**
267
+ * Formats a timestamp to a human-readable string.
268
+ */
269
+ export function formatTimestamp(timestamp: number): string {
270
+ return new Date(timestamp).toLocaleString();
271
+ }
272
+
273
+ /**
274
+ * Returns a CSS class name based on severity.
275
+ */
276
+ export function getSeverityClass(severity: Severity): string {
277
+ switch (severity) {
278
+ case "critical":
279
+ return "audit-severity-critical";
280
+ case "error":
281
+ return "audit-severity-error";
282
+ case "warning":
283
+ return "audit-severity-warning";
284
+ case "info":
285
+ default:
286
+ return "audit-severity-info";
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Returns a color based on severity (for inline styles).
292
+ */
293
+ export function getSeverityColor(severity: Severity): string {
294
+ switch (severity) {
295
+ case "critical":
296
+ return "#dc2626"; // red-600
297
+ case "error":
298
+ return "#ea580c"; // orange-600
299
+ case "warning":
300
+ return "#ca8a04"; // yellow-600
301
+ case "info":
302
+ default:
303
+ return "#2563eb"; // blue-600
304
+ }
305
+ }
package/src/test.ts ADDED
@@ -0,0 +1,18 @@
1
+ /// <reference types="vite/client" />
2
+ import type { TestConvex } from "convex-test";
3
+ import type { GenericSchema, SchemaDefinition } from "convex/server";
4
+ import schema from "./component/schema.js";
5
+ const modules = import.meta.glob("./component/**/*.ts");
6
+
7
+ /**
8
+ * Register the component with the test convex instance.
9
+ * @param t - The test convex instance, e.g. from calling `convexTest`.
10
+ * @param name - The name of the component, as registered in convex.config.ts.
11
+ */
12
+ export function register(
13
+ t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
14
+ name: string = "auditLog",
15
+ ) {
16
+ t.registerComponent(name, schema, modules);
17
+ }
18
+ export default { register, schema, modules };