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.
- package/LICENSE +201 -0
- package/README.md +408 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +336 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +297 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +317 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +341 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +598 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +87 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +71 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/shared.d.ts +203 -0
- package/dist/component/shared.d.ts.map +1 -0
- package/dist/component/shared.js +94 -0
- package/dist/component/shared.js.map +1 -0
- package/dist/react/index.d.ts +247 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +196 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +115 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +61 -0
- package/src/client/index.ts +525 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +392 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.test.ts +171 -0
- package/src/component/lib.ts +722 -0
- package/src/component/schema.ts +93 -0
- package/src/component/setup.test.ts +11 -0
- package/src/component/shared.ts +167 -0
- package/src/react/index.ts +305 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { FunctionReference } from "convex/server";
|
|
2
|
+
export type Severity = "info" | "warning" | "error" | "critical";
|
|
3
|
+
export interface AuditLogEntry {
|
|
4
|
+
_id: string;
|
|
5
|
+
_creationTime: number;
|
|
6
|
+
action: string;
|
|
7
|
+
actorId?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
resourceType?: string;
|
|
10
|
+
resourceId?: string;
|
|
11
|
+
metadata?: unknown;
|
|
12
|
+
severity: Severity;
|
|
13
|
+
ipAddress?: string;
|
|
14
|
+
userAgent?: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
before?: unknown;
|
|
18
|
+
after?: unknown;
|
|
19
|
+
diff?: string;
|
|
20
|
+
retentionCategory?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuditLogStats {
|
|
23
|
+
totalCount: number;
|
|
24
|
+
bySeverity: {
|
|
25
|
+
info: number;
|
|
26
|
+
warning: number;
|
|
27
|
+
error: number;
|
|
28
|
+
critical: number;
|
|
29
|
+
};
|
|
30
|
+
topActions: {
|
|
31
|
+
action: string;
|
|
32
|
+
count: number;
|
|
33
|
+
}[];
|
|
34
|
+
topActors: {
|
|
35
|
+
actorId: string;
|
|
36
|
+
count: number;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export interface Anomaly {
|
|
40
|
+
action: string;
|
|
41
|
+
count: number;
|
|
42
|
+
threshold: number;
|
|
43
|
+
windowMinutes: number;
|
|
44
|
+
detectedAt: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hook to query audit logs by resource.
|
|
48
|
+
* Provides real-time updates when the audit log changes.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* import { useAuditLogByResource } from "convex-audit-log/react";
|
|
53
|
+
* import { api } from "../convex/_generated/api";
|
|
54
|
+
*
|
|
55
|
+
* function DocumentHistory({ documentId }: { documentId: string }) {
|
|
56
|
+
* const logs = useAuditLogByResource(api.auditLog.queryByResource, {
|
|
57
|
+
* resourceType: "documents",
|
|
58
|
+
* resourceId: documentId,
|
|
59
|
+
* limit: 20,
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* if (!logs) return <div>Loading...</div>;
|
|
63
|
+
*
|
|
64
|
+
* return (
|
|
65
|
+
* <ul>
|
|
66
|
+
* {logs.map((log) => (
|
|
67
|
+
* <li key={log._id}>
|
|
68
|
+
* {log.action} by {log.actorId} at {new Date(log.timestamp).toLocaleString()}
|
|
69
|
+
* </li>
|
|
70
|
+
* ))}
|
|
71
|
+
* </ul>
|
|
72
|
+
* );
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function useAuditLogByResource(queryRef: FunctionReference<"query", "public", {
|
|
77
|
+
resourceType: string;
|
|
78
|
+
resourceId: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
fromTimestamp?: number;
|
|
81
|
+
}, AuditLogEntry[]>, args: {
|
|
82
|
+
resourceType: string;
|
|
83
|
+
resourceId: string;
|
|
84
|
+
limit?: number;
|
|
85
|
+
fromTimestamp?: number;
|
|
86
|
+
}): AuditLogEntry[] | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Hook to query audit logs by actor (user).
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* import { useAuditLogByActor } from "convex-audit-log/react";
|
|
93
|
+
* import { api } from "../convex/_generated/api";
|
|
94
|
+
*
|
|
95
|
+
* function UserActivity({ userId }: { userId: string }) {
|
|
96
|
+
* const logs = useAuditLogByActor(api.auditLog.queryByActor, {
|
|
97
|
+
* actorId: userId,
|
|
98
|
+
* limit: 50,
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* if (!logs) return <div>Loading...</div>;
|
|
102
|
+
*
|
|
103
|
+
* return (
|
|
104
|
+
* <ul>
|
|
105
|
+
* {logs.map((log) => (
|
|
106
|
+
* <li key={log._id}>{log.action}</li>
|
|
107
|
+
* ))}
|
|
108
|
+
* </ul>
|
|
109
|
+
* );
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function useAuditLogByActor(queryRef: FunctionReference<"query", "public", {
|
|
114
|
+
actorId: string;
|
|
115
|
+
limit?: number;
|
|
116
|
+
fromTimestamp?: number;
|
|
117
|
+
actions?: string[];
|
|
118
|
+
}, AuditLogEntry[]>, args: {
|
|
119
|
+
actorId: string;
|
|
120
|
+
limit?: number;
|
|
121
|
+
fromTimestamp?: number;
|
|
122
|
+
actions?: string[];
|
|
123
|
+
}): AuditLogEntry[] | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Hook to watch critical events in real-time.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```tsx
|
|
129
|
+
* import { useWatchCriticalEvents } from "convex-audit-log/react";
|
|
130
|
+
* import { api } from "../convex/_generated/api";
|
|
131
|
+
*
|
|
132
|
+
* function SecurityAlerts() {
|
|
133
|
+
* const criticalEvents = useWatchCriticalEvents(api.auditLog.watchCritical, {
|
|
134
|
+
* limit: 10,
|
|
135
|
+
* });
|
|
136
|
+
*
|
|
137
|
+
* if (!criticalEvents) return <div>Loading...</div>;
|
|
138
|
+
*
|
|
139
|
+
* return (
|
|
140
|
+
* <div className="alerts">
|
|
141
|
+
* {criticalEvents.map((event) => (
|
|
142
|
+
* <div key={event._id} className={`alert alert-${event.severity}`}>
|
|
143
|
+
* {event.action}
|
|
144
|
+
* </div>
|
|
145
|
+
* ))}
|
|
146
|
+
* </div>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function useWatchCriticalEvents(queryRef: FunctionReference<"query", "public", {
|
|
152
|
+
severity?: Severity[];
|
|
153
|
+
limit?: number;
|
|
154
|
+
}, AuditLogEntry[]>, args?: {
|
|
155
|
+
severity?: Severity[];
|
|
156
|
+
limit?: number;
|
|
157
|
+
}): AuditLogEntry[] | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* Hook to get audit log statistics.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* import { useAuditLogStats } from "convex-audit-log/react";
|
|
164
|
+
* import { api } from "../convex/_generated/api";
|
|
165
|
+
*
|
|
166
|
+
* function Dashboard() {
|
|
167
|
+
* const stats = useAuditLogStats(api.auditLog.getStats, {
|
|
168
|
+
* fromTimestamp: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
|
|
169
|
+
* });
|
|
170
|
+
*
|
|
171
|
+
* if (!stats) return <div>Loading...</div>;
|
|
172
|
+
*
|
|
173
|
+
* return (
|
|
174
|
+
* <div>
|
|
175
|
+
* <h2>Total Events: {stats.totalCount}</h2>
|
|
176
|
+
* <div>Critical: {stats.bySeverity.critical}</div>
|
|
177
|
+
* <div>Errors: {stats.bySeverity.error}</div>
|
|
178
|
+
* </div>
|
|
179
|
+
* );
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export declare function useAuditLogStats(queryRef: FunctionReference<"query", "public", {
|
|
184
|
+
fromTimestamp?: number;
|
|
185
|
+
toTimestamp?: number;
|
|
186
|
+
}, AuditLogStats>, args?: {
|
|
187
|
+
fromTimestamp?: number;
|
|
188
|
+
toTimestamp?: number;
|
|
189
|
+
}): AuditLogStats | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Hook to detect anomalies in real-time.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* import { useAnomalyDetection } from "convex-audit-log/react";
|
|
196
|
+
* import { api } from "../convex/_generated/api";
|
|
197
|
+
*
|
|
198
|
+
* function AnomalyMonitor() {
|
|
199
|
+
* const anomalies = useAnomalyDetection(api.auditLog.detectAnomalies, {
|
|
200
|
+
* patterns: [
|
|
201
|
+
* { action: "user.login.failed", threshold: 5, windowMinutes: 5 },
|
|
202
|
+
* { action: "record.deleted", threshold: 10, windowMinutes: 1 },
|
|
203
|
+
* ],
|
|
204
|
+
* });
|
|
205
|
+
*
|
|
206
|
+
* if (!anomalies) return <div>Loading...</div>;
|
|
207
|
+
* if (anomalies.length === 0) return <div>No anomalies detected</div>;
|
|
208
|
+
*
|
|
209
|
+
* return (
|
|
210
|
+
* <div className="anomalies">
|
|
211
|
+
* {anomalies.map((anomaly, i) => (
|
|
212
|
+
* <div key={i} className="anomaly-alert">
|
|
213
|
+
* {anomaly.action}: {anomaly.count} events in {anomaly.windowMinutes} minutes
|
|
214
|
+
* (threshold: {anomaly.threshold})
|
|
215
|
+
* </div>
|
|
216
|
+
* ))}
|
|
217
|
+
* </div>
|
|
218
|
+
* );
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export declare function useAnomalyDetection(queryRef: FunctionReference<"query", "public", {
|
|
223
|
+
patterns: {
|
|
224
|
+
action: string;
|
|
225
|
+
threshold: number;
|
|
226
|
+
windowMinutes: number;
|
|
227
|
+
}[];
|
|
228
|
+
}, Anomaly[]>, args: {
|
|
229
|
+
patterns: {
|
|
230
|
+
action: string;
|
|
231
|
+
threshold: number;
|
|
232
|
+
windowMinutes: number;
|
|
233
|
+
}[];
|
|
234
|
+
}): Anomaly[] | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* Formats a timestamp to a human-readable string.
|
|
237
|
+
*/
|
|
238
|
+
export declare function formatTimestamp(timestamp: number): string;
|
|
239
|
+
/**
|
|
240
|
+
* Returns a CSS class name based on severity.
|
|
241
|
+
*/
|
|
242
|
+
export declare function getSeverityClass(severity: Severity): string;
|
|
243
|
+
/**
|
|
244
|
+
* Returns a color based on severity (for inline styles).
|
|
245
|
+
*/
|
|
246
|
+
export declare function getSeverityColor(severity: Severity): string;
|
|
247
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useQuery } from "convex/react";
|
|
3
|
+
/**
|
|
4
|
+
* Hook to query audit logs by resource.
|
|
5
|
+
* Provides real-time updates when the audit log changes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { useAuditLogByResource } from "convex-audit-log/react";
|
|
10
|
+
* import { api } from "../convex/_generated/api";
|
|
11
|
+
*
|
|
12
|
+
* function DocumentHistory({ documentId }: { documentId: string }) {
|
|
13
|
+
* const logs = useAuditLogByResource(api.auditLog.queryByResource, {
|
|
14
|
+
* resourceType: "documents",
|
|
15
|
+
* resourceId: documentId,
|
|
16
|
+
* limit: 20,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* if (!logs) return <div>Loading...</div>;
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <ul>
|
|
23
|
+
* {logs.map((log) => (
|
|
24
|
+
* <li key={log._id}>
|
|
25
|
+
* {log.action} by {log.actorId} at {new Date(log.timestamp).toLocaleString()}
|
|
26
|
+
* </li>
|
|
27
|
+
* ))}
|
|
28
|
+
* </ul>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useAuditLogByResource(queryRef, args) {
|
|
34
|
+
return useQuery(queryRef, args);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Hook to query audit logs by actor (user).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import { useAuditLogByActor } from "convex-audit-log/react";
|
|
42
|
+
* import { api } from "../convex/_generated/api";
|
|
43
|
+
*
|
|
44
|
+
* function UserActivity({ userId }: { userId: string }) {
|
|
45
|
+
* const logs = useAuditLogByActor(api.auditLog.queryByActor, {
|
|
46
|
+
* actorId: userId,
|
|
47
|
+
* limit: 50,
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* if (!logs) return <div>Loading...</div>;
|
|
51
|
+
*
|
|
52
|
+
* return (
|
|
53
|
+
* <ul>
|
|
54
|
+
* {logs.map((log) => (
|
|
55
|
+
* <li key={log._id}>{log.action}</li>
|
|
56
|
+
* ))}
|
|
57
|
+
* </ul>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useAuditLogByActor(queryRef, args) {
|
|
63
|
+
return useQuery(queryRef, args);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Hook to watch critical events in real-time.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { useWatchCriticalEvents } from "convex-audit-log/react";
|
|
71
|
+
* import { api } from "../convex/_generated/api";
|
|
72
|
+
*
|
|
73
|
+
* function SecurityAlerts() {
|
|
74
|
+
* const criticalEvents = useWatchCriticalEvents(api.auditLog.watchCritical, {
|
|
75
|
+
* limit: 10,
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* if (!criticalEvents) return <div>Loading...</div>;
|
|
79
|
+
*
|
|
80
|
+
* return (
|
|
81
|
+
* <div className="alerts">
|
|
82
|
+
* {criticalEvents.map((event) => (
|
|
83
|
+
* <div key={event._id} className={`alert alert-${event.severity}`}>
|
|
84
|
+
* {event.action}
|
|
85
|
+
* </div>
|
|
86
|
+
* ))}
|
|
87
|
+
* </div>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function useWatchCriticalEvents(queryRef, args) {
|
|
93
|
+
return useQuery(queryRef, args ?? {});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Hook to get audit log statistics.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { useAuditLogStats } from "convex-audit-log/react";
|
|
101
|
+
* import { api } from "../convex/_generated/api";
|
|
102
|
+
*
|
|
103
|
+
* function Dashboard() {
|
|
104
|
+
* const stats = useAuditLogStats(api.auditLog.getStats, {
|
|
105
|
+
* fromTimestamp: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* if (!stats) return <div>Loading...</div>;
|
|
109
|
+
*
|
|
110
|
+
* return (
|
|
111
|
+
* <div>
|
|
112
|
+
* <h2>Total Events: {stats.totalCount}</h2>
|
|
113
|
+
* <div>Critical: {stats.bySeverity.critical}</div>
|
|
114
|
+
* <div>Errors: {stats.bySeverity.error}</div>
|
|
115
|
+
* </div>
|
|
116
|
+
* );
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function useAuditLogStats(queryRef, args) {
|
|
121
|
+
return useQuery(queryRef, args ?? {});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Hook to detect anomalies in real-time.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* import { useAnomalyDetection } from "convex-audit-log/react";
|
|
129
|
+
* import { api } from "../convex/_generated/api";
|
|
130
|
+
*
|
|
131
|
+
* function AnomalyMonitor() {
|
|
132
|
+
* const anomalies = useAnomalyDetection(api.auditLog.detectAnomalies, {
|
|
133
|
+
* patterns: [
|
|
134
|
+
* { action: "user.login.failed", threshold: 5, windowMinutes: 5 },
|
|
135
|
+
* { action: "record.deleted", threshold: 10, windowMinutes: 1 },
|
|
136
|
+
* ],
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* if (!anomalies) return <div>Loading...</div>;
|
|
140
|
+
* if (anomalies.length === 0) return <div>No anomalies detected</div>;
|
|
141
|
+
*
|
|
142
|
+
* return (
|
|
143
|
+
* <div className="anomalies">
|
|
144
|
+
* {anomalies.map((anomaly, i) => (
|
|
145
|
+
* <div key={i} className="anomaly-alert">
|
|
146
|
+
* {anomaly.action}: {anomaly.count} events in {anomaly.windowMinutes} minutes
|
|
147
|
+
* (threshold: {anomaly.threshold})
|
|
148
|
+
* </div>
|
|
149
|
+
* ))}
|
|
150
|
+
* </div>
|
|
151
|
+
* );
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function useAnomalyDetection(queryRef, args) {
|
|
156
|
+
return useQuery(queryRef, args);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Formats a timestamp to a human-readable string.
|
|
160
|
+
*/
|
|
161
|
+
export function formatTimestamp(timestamp) {
|
|
162
|
+
return new Date(timestamp).toLocaleString();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Returns a CSS class name based on severity.
|
|
166
|
+
*/
|
|
167
|
+
export function getSeverityClass(severity) {
|
|
168
|
+
switch (severity) {
|
|
169
|
+
case "critical":
|
|
170
|
+
return "audit-severity-critical";
|
|
171
|
+
case "error":
|
|
172
|
+
return "audit-severity-error";
|
|
173
|
+
case "warning":
|
|
174
|
+
return "audit-severity-warning";
|
|
175
|
+
case "info":
|
|
176
|
+
default:
|
|
177
|
+
return "audit-severity-info";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Returns a color based on severity (for inline styles).
|
|
182
|
+
*/
|
|
183
|
+
export function getSeverityColor(severity) {
|
|
184
|
+
switch (severity) {
|
|
185
|
+
case "critical":
|
|
186
|
+
return "#dc2626"; // red-600
|
|
187
|
+
case "error":
|
|
188
|
+
return "#ea580c"; // orange-600
|
|
189
|
+
case "warning":
|
|
190
|
+
return "#ca8a04"; // yellow-600
|
|
191
|
+
case "info":
|
|
192
|
+
default:
|
|
193
|
+
return "#2563eb"; // blue-600
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AA6CxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAKC,EACD,IAKC;IAED,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAKC,EACD,IAKC;IAED,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAKC,EACD,IAGC;IAED,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAKC,EACD,IAGC;IAED,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAKC,EACD,IAEC;IAED,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC;AAC9C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,yBAAyB,CAAC;QACnC,KAAK,OAAO;YACV,OAAO,sBAAsB,CAAC;QAChC,KAAK,SAAS;YACZ,OAAO,wBAAwB,CAAC;QAClC,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,qBAAqB,CAAC;IACjC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,SAAS,CAAC,CAAC,UAAU;QAC9B,KAAK,OAAO;YACV,OAAO,SAAS,CAAC,CAAC,aAAa;QACjC,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC,CAAC,aAAa;QACjC,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,SAAS,CAAC,CAAC,WAAW;IACjC,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "convex-audit-log",
|
|
3
|
+
"description": "A comprehensive audit logging component for Convex - track user actions, API calls, and system events with built-in compliance features.",
|
|
4
|
+
"repository": "github:robertalv/audit-log",
|
|
5
|
+
"homepage": "https://github.com/robertalv/audit-log#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/robertalv/audit-log/issues"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"convex",
|
|
13
|
+
"component",
|
|
14
|
+
"audit-log",
|
|
15
|
+
"logging",
|
|
16
|
+
"compliance",
|
|
17
|
+
"analytics",
|
|
18
|
+
"security",
|
|
19
|
+
"GDPR",
|
|
20
|
+
"HIPAA",
|
|
21
|
+
"SOC2"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "run-p -r 'dev:*'",
|
|
26
|
+
"dev:backend": "convex dev --typecheck-components",
|
|
27
|
+
"dev:frontend": "cd example && vite --clearScreen false",
|
|
28
|
+
"dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial",
|
|
29
|
+
"predev": "path-exists .env.local dist || (npm run build && convex dev --once)",
|
|
30
|
+
"build": "tsc --project ./tsconfig.build.json",
|
|
31
|
+
"build:codegen": "npx convex codegen --component-dir ./src/component && npm run build",
|
|
32
|
+
"build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen",
|
|
33
|
+
"typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"all": "run-p -r 'dev:*' 'test:watch'",
|
|
36
|
+
"test": "vitest run --typecheck",
|
|
37
|
+
"test:watch": "vitest --typecheck --clearScreen false",
|
|
38
|
+
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
39
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
40
|
+
"preversion": "npm ci && npm run build:clean && run-p test lint typecheck",
|
|
41
|
+
"alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
|
|
42
|
+
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
43
|
+
"version": "vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src"
|
|
48
|
+
],
|
|
49
|
+
"exports": {
|
|
50
|
+
"./package.json": "./package.json",
|
|
51
|
+
".": {
|
|
52
|
+
"types": "./dist/client/index.d.ts",
|
|
53
|
+
"default": "./dist/client/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./react": {
|
|
56
|
+
"types": "./dist/react/index.d.ts",
|
|
57
|
+
"default": "./dist/react/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./test": "./src/test.ts",
|
|
60
|
+
"./_generated/component.js": {
|
|
61
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./_generated/component": {
|
|
64
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
65
|
+
},
|
|
66
|
+
"./convex.config.js": {
|
|
67
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
68
|
+
"default": "./dist/component/convex.config.js"
|
|
69
|
+
},
|
|
70
|
+
"./convex.config": {
|
|
71
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
72
|
+
"default": "./dist/component/convex.config.js"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"convex": "^1.29.3",
|
|
77
|
+
"react": "^18.3.1 || ^19.0.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"react": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@convex-dev/eslint-plugin": "^1.1.1",
|
|
86
|
+
"@edge-runtime/vm": "^5.0.0",
|
|
87
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
88
|
+
"@eslint/js": "9.39.1",
|
|
89
|
+
"@types/node": "^24.10.4",
|
|
90
|
+
"@types/react": "^19.2.7",
|
|
91
|
+
"@types/react-dom": "^19.2.3",
|
|
92
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
93
|
+
"chokidar-cli": "3.0.0",
|
|
94
|
+
"convex": "1.31.0",
|
|
95
|
+
"convex-test": "0.0.40",
|
|
96
|
+
"cpy-cli": "^6.0.0",
|
|
97
|
+
"eslint": "9.39.1",
|
|
98
|
+
"eslint-plugin-react": "^7.37.5",
|
|
99
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
100
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
101
|
+
"globals": "^17.0.0",
|
|
102
|
+
"npm-run-all2": "8.0.4",
|
|
103
|
+
"path-exists-cli": "2.0.0",
|
|
104
|
+
"pkg-pr-new": "^0.0.60",
|
|
105
|
+
"prettier": "3.6.2",
|
|
106
|
+
"react": "^19.2.1",
|
|
107
|
+
"react-dom": "^19.2.1",
|
|
108
|
+
"typescript": "5.9.3",
|
|
109
|
+
"typescript-eslint": "8.47.0",
|
|
110
|
+
"vite": "7.2.6",
|
|
111
|
+
"vitest": "4.0.17"
|
|
112
|
+
},
|
|
113
|
+
"types": "./dist/client/index.d.ts",
|
|
114
|
+
"module": "./dist/client/index.js"
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This is only here so convex-test can detect a _generated folder
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { exposeAuditLogApi } from "./index.js";
|
|
3
|
+
import { anyApi, type ApiFromModules } from "convex/server";
|
|
4
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
5
|
+
|
|
6
|
+
// Re-export the audit log API for testing
|
|
7
|
+
export const { queryByResource, queryByActor, getStats, watchCritical } =
|
|
8
|
+
exposeAuditLogApi(components.auditLog, {
|
|
9
|
+
auth: async (ctx, _operation) => {
|
|
10
|
+
return (await ctx.auth.getUserIdentity())?.subject ?? "anonymous";
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const testApi = (
|
|
15
|
+
anyApi as unknown as ApiFromModules<{
|
|
16
|
+
"index.test": {
|
|
17
|
+
queryByResource: typeof queryByResource;
|
|
18
|
+
queryByActor: typeof queryByActor;
|
|
19
|
+
getStats: typeof getStats;
|
|
20
|
+
watchCritical: typeof watchCritical;
|
|
21
|
+
};
|
|
22
|
+
}>
|
|
23
|
+
)["index.test"];
|
|
24
|
+
|
|
25
|
+
describe("client tests", () => {
|
|
26
|
+
test("should be able to query by resource (empty at start)", async () => {
|
|
27
|
+
const t = initConvexTest().withIdentity({
|
|
28
|
+
subject: "user1",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const logs = await t.query(testApi.queryByResource, {
|
|
32
|
+
resourceType: "documents",
|
|
33
|
+
resourceId: "doc123",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(logs).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("should be able to get stats (empty at start)", async () => {
|
|
40
|
+
const t = initConvexTest().withIdentity({
|
|
41
|
+
subject: "user1",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const stats = await t.query(testApi.getStats, {});
|
|
45
|
+
|
|
46
|
+
expect(stats).toBeDefined();
|
|
47
|
+
expect(stats.totalCount).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("should be able to watch critical events (empty at start)", async () => {
|
|
51
|
+
const t = initConvexTest().withIdentity({
|
|
52
|
+
subject: "user1",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const criticalEvents = await t.query(testApi.watchCritical, {
|
|
56
|
+
limit: 10,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(criticalEvents).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
});
|