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,525 @@
|
|
|
1
|
+
import {
|
|
2
|
+
queryGeneric,
|
|
3
|
+
mutationGeneric,
|
|
4
|
+
} from "convex/server";
|
|
5
|
+
import type {
|
|
6
|
+
Auth,
|
|
7
|
+
GenericDataModel,
|
|
8
|
+
GenericQueryCtx,
|
|
9
|
+
GenericMutationCtx,
|
|
10
|
+
} from "convex/server";
|
|
11
|
+
import { v } from "convex/values";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The component API type. This is a placeholder that will be properly typed
|
|
15
|
+
* when the user imports from their generated component types.
|
|
16
|
+
*/
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
export type AuditLogComponentApi = any;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Severity levels for audit events.
|
|
22
|
+
*/
|
|
23
|
+
export type Severity = "info" | "warning" | "error" | "critical";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Input for creating an audit event.
|
|
27
|
+
*/
|
|
28
|
+
export interface AuditEventInput {
|
|
29
|
+
action: string;
|
|
30
|
+
actorId?: string;
|
|
31
|
+
resourceType?: string;
|
|
32
|
+
resourceId?: string;
|
|
33
|
+
metadata?: unknown;
|
|
34
|
+
severity: Severity;
|
|
35
|
+
ipAddress?: string;
|
|
36
|
+
userAgent?: string;
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
retentionCategory?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Input for creating a change tracking event.
|
|
44
|
+
*/
|
|
45
|
+
export interface ChangeEventInput {
|
|
46
|
+
action: string;
|
|
47
|
+
actorId?: string;
|
|
48
|
+
resourceType: string;
|
|
49
|
+
resourceId: string;
|
|
50
|
+
before?: unknown;
|
|
51
|
+
after?: unknown;
|
|
52
|
+
generateDiff?: boolean;
|
|
53
|
+
severity?: Severity;
|
|
54
|
+
ipAddress?: string;
|
|
55
|
+
userAgent?: string;
|
|
56
|
+
sessionId?: string;
|
|
57
|
+
tags?: string[];
|
|
58
|
+
retentionCategory?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Query filters for searching audit logs.
|
|
63
|
+
*/
|
|
64
|
+
export interface QueryFilters {
|
|
65
|
+
severity?: Severity[];
|
|
66
|
+
actions?: string[];
|
|
67
|
+
resourceTypes?: string[];
|
|
68
|
+
actorIds?: string[];
|
|
69
|
+
fromTimestamp?: number;
|
|
70
|
+
toTimestamp?: number;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pagination options.
|
|
76
|
+
*/
|
|
77
|
+
export interface PaginationOptions {
|
|
78
|
+
cursor?: string;
|
|
79
|
+
limit: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Anomaly detection pattern.
|
|
84
|
+
*/
|
|
85
|
+
export interface AnomalyPattern {
|
|
86
|
+
action: string;
|
|
87
|
+
threshold: number;
|
|
88
|
+
windowMinutes: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Cleanup options for retention policies.
|
|
93
|
+
*/
|
|
94
|
+
export interface CleanupOptions {
|
|
95
|
+
olderThanDays?: number;
|
|
96
|
+
preserveSeverity?: Severity[];
|
|
97
|
+
retentionCategory?: string;
|
|
98
|
+
batchSize?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Configuration options for the audit log component.
|
|
103
|
+
*/
|
|
104
|
+
export interface AuditLogOptions {
|
|
105
|
+
defaultRetentionDays?: number;
|
|
106
|
+
criticalRetentionDays?: number;
|
|
107
|
+
piiFields?: string[];
|
|
108
|
+
samplingEnabled?: boolean;
|
|
109
|
+
samplingRate?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
113
|
+
type MutationCtx = Pick<
|
|
114
|
+
GenericMutationCtx<GenericDataModel>,
|
|
115
|
+
"runQuery" | "runMutation"
|
|
116
|
+
>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Client wrapper for the Audit Log component.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* import { AuditLog } from "convex-audit-log";
|
|
124
|
+
* import { components } from "./_generated/api";
|
|
125
|
+
*
|
|
126
|
+
* const auditLog = new AuditLog(components.auditLog, {
|
|
127
|
+
* piiFields: ["email", "phone", "ssn"],
|
|
128
|
+
* });
|
|
129
|
+
*
|
|
130
|
+
* // In your mutation
|
|
131
|
+
* await auditLog.log(ctx, {
|
|
132
|
+
* action: "user.login",
|
|
133
|
+
* actorId: userId,
|
|
134
|
+
* severity: "info",
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export class AuditLog {
|
|
139
|
+
public component: AuditLogComponentApi;
|
|
140
|
+
public options: AuditLogOptions;
|
|
141
|
+
private piiFields: Set<string>;
|
|
142
|
+
|
|
143
|
+
constructor(component: AuditLogComponentApi, options: AuditLogOptions = {}) {
|
|
144
|
+
this.component = component;
|
|
145
|
+
this.options = options;
|
|
146
|
+
this.piiFields = new Set(options.piiFields ?? []);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Log a single audit event.
|
|
151
|
+
*/
|
|
152
|
+
async log(ctx: MutationCtx, event: AuditEventInput): Promise<string> {
|
|
153
|
+
if (this.options.samplingEnabled && this.options.samplingRate) {
|
|
154
|
+
if (Math.random() > this.options.samplingRate) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sanitizedEvent = this.redactPII(event);
|
|
160
|
+
|
|
161
|
+
return await ctx.runMutation(this.component.lib.log, sanitizedEvent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Log a change event with before/after states.
|
|
166
|
+
*/
|
|
167
|
+
async logChange(ctx: MutationCtx, event: ChangeEventInput): Promise<string> {
|
|
168
|
+
const sanitizedEvent = {
|
|
169
|
+
...event,
|
|
170
|
+
before: event.before ? this.redactPIIFromObject(event.before) : undefined,
|
|
171
|
+
after: event.after ? this.redactPIIFromObject(event.after) : undefined,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return await ctx.runMutation(this.component.lib.logChange, sanitizedEvent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Log multiple events in a single transaction.
|
|
179
|
+
*/
|
|
180
|
+
async logBulk(ctx: MutationCtx, events: AuditEventInput[]): Promise<string[]> {
|
|
181
|
+
const sanitizedEvents = events.map((event) => this.redactPII(event));
|
|
182
|
+
return await ctx.runMutation(this.component.lib.logBulk, {
|
|
183
|
+
events: sanitizedEvents,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Query audit logs by resource.
|
|
189
|
+
*/
|
|
190
|
+
async queryByResource(
|
|
191
|
+
ctx: QueryCtx,
|
|
192
|
+
args: {
|
|
193
|
+
resourceType: string;
|
|
194
|
+
resourceId: string;
|
|
195
|
+
limit?: number;
|
|
196
|
+
fromTimestamp?: number;
|
|
197
|
+
}
|
|
198
|
+
) {
|
|
199
|
+
return await ctx.runQuery(this.component.lib.queryByResource, args);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Query audit logs by actor (user).
|
|
204
|
+
*/
|
|
205
|
+
async queryByActor(
|
|
206
|
+
ctx: QueryCtx,
|
|
207
|
+
args: {
|
|
208
|
+
actorId: string;
|
|
209
|
+
limit?: number;
|
|
210
|
+
fromTimestamp?: number;
|
|
211
|
+
actions?: string[];
|
|
212
|
+
}
|
|
213
|
+
) {
|
|
214
|
+
return await ctx.runQuery(this.component.lib.queryByActor, args);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Query audit logs by severity.
|
|
219
|
+
*/
|
|
220
|
+
async queryBySeverity(
|
|
221
|
+
ctx: QueryCtx,
|
|
222
|
+
args: {
|
|
223
|
+
severity: Severity[];
|
|
224
|
+
limit?: number;
|
|
225
|
+
fromTimestamp?: number;
|
|
226
|
+
}
|
|
227
|
+
) {
|
|
228
|
+
return await ctx.runQuery(this.component.lib.queryBySeverity, args);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Query audit logs by action.
|
|
233
|
+
*/
|
|
234
|
+
async queryByAction(
|
|
235
|
+
ctx: QueryCtx,
|
|
236
|
+
args: {
|
|
237
|
+
action: string;
|
|
238
|
+
limit?: number;
|
|
239
|
+
fromTimestamp?: number;
|
|
240
|
+
}
|
|
241
|
+
) {
|
|
242
|
+
return await ctx.runQuery(this.component.lib.queryByAction, args);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Advanced search with multiple filters.
|
|
247
|
+
*/
|
|
248
|
+
async search(
|
|
249
|
+
ctx: QueryCtx,
|
|
250
|
+
args: {
|
|
251
|
+
filters: QueryFilters;
|
|
252
|
+
pagination: PaginationOptions;
|
|
253
|
+
}
|
|
254
|
+
) {
|
|
255
|
+
return await ctx.runQuery(this.component.lib.search, args);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Watch for critical events (real-time subscription).
|
|
260
|
+
*/
|
|
261
|
+
async watchCritical(
|
|
262
|
+
ctx: QueryCtx,
|
|
263
|
+
args?: {
|
|
264
|
+
severity?: Severity[];
|
|
265
|
+
limit?: number;
|
|
266
|
+
}
|
|
267
|
+
) {
|
|
268
|
+
return await ctx.runQuery(this.component.lib.watchCritical, args ?? {});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get a single audit log by ID.
|
|
273
|
+
*/
|
|
274
|
+
async get(ctx: QueryCtx, id: string) {
|
|
275
|
+
return await ctx.runQuery(this.component.lib.get, { id });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get statistics for audit logs.
|
|
280
|
+
*/
|
|
281
|
+
async getStats(
|
|
282
|
+
ctx: QueryCtx,
|
|
283
|
+
args?: {
|
|
284
|
+
fromTimestamp?: number;
|
|
285
|
+
toTimestamp?: number;
|
|
286
|
+
}
|
|
287
|
+
) {
|
|
288
|
+
return await ctx.runQuery(this.component.lib.getStats, args ?? {});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Detect anomalies based on event patterns.
|
|
293
|
+
*/
|
|
294
|
+
async detectAnomalies(ctx: QueryCtx, patterns: AnomalyPattern[]) {
|
|
295
|
+
return await ctx.runQuery(this.component.lib.detectAnomalies, { patterns });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate a report of audit logs.
|
|
300
|
+
*/
|
|
301
|
+
async generateReport(
|
|
302
|
+
ctx: QueryCtx,
|
|
303
|
+
args: {
|
|
304
|
+
startDate: number;
|
|
305
|
+
endDate: number;
|
|
306
|
+
format: "json" | "csv";
|
|
307
|
+
includeFields?: string[];
|
|
308
|
+
groupBy?: string;
|
|
309
|
+
}
|
|
310
|
+
) {
|
|
311
|
+
return await ctx.runQuery(this.component.lib.generateReport, args);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Clean up old audit logs based on retention policies.
|
|
316
|
+
*/
|
|
317
|
+
async cleanup(ctx: MutationCtx, options?: CleanupOptions): Promise<number> {
|
|
318
|
+
return await ctx.runMutation(this.component.lib.cleanup, options ?? {});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get current configuration.
|
|
323
|
+
*/
|
|
324
|
+
async getConfig(ctx: QueryCtx) {
|
|
325
|
+
return await ctx.runQuery(this.component.lib.getConfig, {});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Update configuration.
|
|
330
|
+
*/
|
|
331
|
+
async updateConfig(
|
|
332
|
+
ctx: MutationCtx,
|
|
333
|
+
options: {
|
|
334
|
+
defaultRetentionDays?: number;
|
|
335
|
+
criticalRetentionDays?: number;
|
|
336
|
+
piiFieldsToRedact?: string[];
|
|
337
|
+
samplingEnabled?: boolean;
|
|
338
|
+
samplingRate?: number;
|
|
339
|
+
customRetention?: { category: string; retentionDays: number }[];
|
|
340
|
+
}
|
|
341
|
+
) {
|
|
342
|
+
return await ctx.runMutation(this.component.lib.updateConfig, options);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private redactPII(event: AuditEventInput): AuditEventInput {
|
|
346
|
+
if (!event.metadata || this.piiFields.size === 0) {
|
|
347
|
+
return event;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
...event,
|
|
352
|
+
metadata: this.redactPIIFromObject(event.metadata),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private redactPIIFromObject(obj: unknown): unknown {
|
|
357
|
+
if (typeof obj !== "object" || obj === null) {
|
|
358
|
+
return obj;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (Array.isArray(obj)) {
|
|
362
|
+
return obj.map((item) => this.redactPIIFromObject(item));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const result: Record<string, unknown> = {};
|
|
366
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
367
|
+
if (this.piiFields.has(key.toLowerCase())) {
|
|
368
|
+
result[key] = "[REDACTED]";
|
|
369
|
+
} else if (typeof value === "object" && value !== null) {
|
|
370
|
+
result[key] = this.redactPIIFromObject(value);
|
|
371
|
+
} else {
|
|
372
|
+
result[key] = value;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Create an exposed API for the audit log component.
|
|
382
|
+
* This allows you to re-export functions that can be called from React clients.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* // In convex/auditLog.ts
|
|
387
|
+
* import { exposeAuditLogApi } from "convex-audit-log";
|
|
388
|
+
* import { components } from "./_generated/api";
|
|
389
|
+
*
|
|
390
|
+
* export const { queryByResource, queryByActor, getStats } = exposeAuditLogApi(
|
|
391
|
+
* components.auditLog,
|
|
392
|
+
* {
|
|
393
|
+
* auth: async (ctx, operation) => {
|
|
394
|
+
* const userId = await getAuthUserId(ctx);
|
|
395
|
+
* if (!userId) throw new Error("Unauthorized");
|
|
396
|
+
* return userId;
|
|
397
|
+
* },
|
|
398
|
+
* }
|
|
399
|
+
* );
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
export function exposeAuditLogApi(
|
|
403
|
+
component: AuditLogComponentApi,
|
|
404
|
+
options: {
|
|
405
|
+
/**
|
|
406
|
+
* Authentication function that runs before each operation.
|
|
407
|
+
* Should throw an error if the user is not authorized.
|
|
408
|
+
*/
|
|
409
|
+
auth: (
|
|
410
|
+
ctx: { auth: Auth },
|
|
411
|
+
operation:
|
|
412
|
+
| { type: "read" }
|
|
413
|
+
| { type: "write" }
|
|
414
|
+
| { type: "admin" }
|
|
415
|
+
) => Promise<string>;
|
|
416
|
+
}
|
|
417
|
+
) {
|
|
418
|
+
return {
|
|
419
|
+
/**
|
|
420
|
+
* Query audit logs by resource.
|
|
421
|
+
*/
|
|
422
|
+
queryByResource: queryGeneric({
|
|
423
|
+
args: {
|
|
424
|
+
resourceType: v.string(),
|
|
425
|
+
resourceId: v.string(),
|
|
426
|
+
limit: v.optional(v.number()),
|
|
427
|
+
fromTimestamp: v.optional(v.number()),
|
|
428
|
+
},
|
|
429
|
+
handler: async (ctx, args) => {
|
|
430
|
+
await options.auth(ctx, { type: "read" });
|
|
431
|
+
return await ctx.runQuery(component.lib.queryByResource, args);
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Query audit logs by actor.
|
|
437
|
+
*/
|
|
438
|
+
queryByActor: queryGeneric({
|
|
439
|
+
args: {
|
|
440
|
+
actorId: v.string(),
|
|
441
|
+
limit: v.optional(v.number()),
|
|
442
|
+
fromTimestamp: v.optional(v.number()),
|
|
443
|
+
actions: v.optional(v.array(v.string())),
|
|
444
|
+
},
|
|
445
|
+
handler: async (ctx, args) => {
|
|
446
|
+
await options.auth(ctx, { type: "read" });
|
|
447
|
+
return await ctx.runQuery(component.lib.queryByActor, args);
|
|
448
|
+
},
|
|
449
|
+
}),
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get statistics.
|
|
453
|
+
*/
|
|
454
|
+
getStats: queryGeneric({
|
|
455
|
+
args: {
|
|
456
|
+
fromTimestamp: v.optional(v.number()),
|
|
457
|
+
toTimestamp: v.optional(v.number()),
|
|
458
|
+
},
|
|
459
|
+
handler: async (ctx, args) => {
|
|
460
|
+
await options.auth(ctx, { type: "read" });
|
|
461
|
+
return await ctx.runQuery(component.lib.getStats, args);
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Watch critical events.
|
|
467
|
+
*/
|
|
468
|
+
watchCritical: queryGeneric({
|
|
469
|
+
args: {
|
|
470
|
+
limit: v.optional(v.number()),
|
|
471
|
+
},
|
|
472
|
+
handler: async (ctx, args) => {
|
|
473
|
+
await options.auth(ctx, { type: "read" });
|
|
474
|
+
return await ctx.runQuery(component.lib.watchCritical, args);
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Clean up old logs (admin only).
|
|
480
|
+
*/
|
|
481
|
+
cleanup: mutationGeneric({
|
|
482
|
+
args: {
|
|
483
|
+
olderThanDays: v.optional(v.number()),
|
|
484
|
+
batchSize: v.optional(v.number()),
|
|
485
|
+
},
|
|
486
|
+
handler: async (ctx, args) => {
|
|
487
|
+
await options.auth(ctx, { type: "admin" });
|
|
488
|
+
return await ctx.runMutation(component.lib.cleanup, args);
|
|
489
|
+
},
|
|
490
|
+
}),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Common action names for consistency.
|
|
496
|
+
*/
|
|
497
|
+
export const AuditActions = {
|
|
498
|
+
USER_LOGIN: "user.login",
|
|
499
|
+
USER_LOGIN_FAILED: "user.login.failed",
|
|
500
|
+
USER_LOGOUT: "user.logout",
|
|
501
|
+
USER_SIGNUP: "user.signup",
|
|
502
|
+
PASSWORD_CHANGED: "user.password.changed",
|
|
503
|
+
PASSWORD_RESET_REQUESTED: "user.password.reset_requested",
|
|
504
|
+
PASSWORD_RESET_COMPLETED: "user.password.reset_completed",
|
|
505
|
+
MFA_ENABLED: "user.mfa.enabled",
|
|
506
|
+
MFA_DISABLED: "user.mfa.disabled",
|
|
507
|
+
USER_CREATED: "user.created",
|
|
508
|
+
USER_UPDATED: "user.updated",
|
|
509
|
+
USER_DELETED: "user.deleted",
|
|
510
|
+
USER_ROLE_CHANGED: "user.role.changed",
|
|
511
|
+
USER_PERMISSIONS_CHANGED: "user.permissions.changed",
|
|
512
|
+
RECORD_CREATED: "record.created",
|
|
513
|
+
RECORD_UPDATED: "record.updated",
|
|
514
|
+
RECORD_DELETED: "record.deleted",
|
|
515
|
+
RECORD_VIEWED: "record.viewed",
|
|
516
|
+
RECORD_EXPORTED: "record.exported",
|
|
517
|
+
SETTINGS_CHANGED: "settings.changed",
|
|
518
|
+
API_KEY_CREATED: "api_key.created",
|
|
519
|
+
API_KEY_REVOKED: "api_key.revoked",
|
|
520
|
+
UNAUTHORIZED_ACCESS: "security.unauthorized_access",
|
|
521
|
+
SUSPICIOUS_ACTIVITY: "security.suspicious_activity",
|
|
522
|
+
RATE_LIMIT_EXCEEDED: "security.rate_limit_exceeded",
|
|
523
|
+
} as const;
|
|
524
|
+
|
|
525
|
+
export type AuditAction = (typeof AuditActions)[keyof typeof AuditActions];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { convexTest } from "convex-test";
|
|
4
|
+
export const modules = import.meta.glob("./**/*.*s");
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
defineSchema,
|
|
8
|
+
type GenericSchema,
|
|
9
|
+
type SchemaDefinition,
|
|
10
|
+
} from "convex/server";
|
|
11
|
+
import { type ComponentApi } from "../component/_generated/component.js";
|
|
12
|
+
import { componentsGeneric } from "convex/server";
|
|
13
|
+
import { register } from "../test.js";
|
|
14
|
+
|
|
15
|
+
export function initConvexTest<
|
|
16
|
+
Schema extends SchemaDefinition<GenericSchema, boolean>,
|
|
17
|
+
>(schema?: Schema) {
|
|
18
|
+
const t = convexTest(schema ?? defineSchema({}), modules);
|
|
19
|
+
register(t);
|
|
20
|
+
return t;
|
|
21
|
+
}
|
|
22
|
+
export const components = componentsGeneric() as unknown as {
|
|
23
|
+
auditLog: ComponentApi;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
test("setup", () => {});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated `api` utility.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as lib from "../lib.js";
|
|
12
|
+
import type * as shared from "../shared.js";
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ApiFromModules,
|
|
16
|
+
FilterApi,
|
|
17
|
+
FunctionReference,
|
|
18
|
+
} from "convex/server";
|
|
19
|
+
import { anyApi, componentsGeneric } from "convex/server";
|
|
20
|
+
|
|
21
|
+
const fullApi: ApiFromModules<{
|
|
22
|
+
lib: typeof lib;
|
|
23
|
+
shared: typeof shared;
|
|
24
|
+
}> = anyApi as any;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A utility for referencing Convex functions in your app's public API.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ```js
|
|
31
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const api: FilterApi<
|
|
35
|
+
typeof fullApi,
|
|
36
|
+
FunctionReference<any, "public">
|
|
37
|
+
> = anyApi as any;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A utility for referencing Convex functions in your app's internal API.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* ```js
|
|
44
|
+
* const myFunctionReference = internal.myModule.myFunction;
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const internal: FilterApi<
|
|
48
|
+
typeof fullApi,
|
|
49
|
+
FunctionReference<any, "internal">
|
|
50
|
+
> = anyApi as any;
|
|
51
|
+
|
|
52
|
+
export const components = componentsGeneric() as unknown as {};
|