flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/app/lib/audit.server.ts

65 lines2,572 bytesCodeBlame
1import { env } from "cloudflare:workers";
2
3import { type AuditEntry, type AuditQuery, type Viewer, auditClient } from "@g1t/contracts";
4
5import { retainedSince, visibilityFor } from "./audit";
6import { instrumented } from "./perf.server";
7import { billing } from "./services.server";
8import { roleIn } from "./session.server";
9
10/** The audit log, which the events service keeps. */
11export const audit = auditClient(instrumented("events", env.EVENTS));
12
13/**
14 * How many days of the workspace's log are kept: 90, the same on every
15 * plan. Null when billing cannot say, and then nothing is held back.
16 */
17export async function auditRetention(workspace: string): Promise<number | null> {
18 const found = await billing.entitlements(workspace.toLowerCase()).catch(() => null);
19 return found?.auditRetentionDays ?? null;
20}
21
22/** The most rows one export writes. */
23export const EXPORT_LIMIT = 10_000;
24
25/**
26 * Entries of a workspace's log the viewer may see, or null when they may
27 * see none of it.
28 */
29export async function auditPage(viewer: Viewer, query: Omit<AuditQuery, "visibility">) {
30 const visibility = viewer ? visibilityFor(roleIn(viewer, query.workspace), viewer.username) : null;
31 if (!visibility) return null;
32 // Reads and exports go back only as far as the workspace's plan keeps.
33 const days = await auditRetention(query.workspace);
34 const since = days == null ? query.since : retainedSince(query.since, days);
35 return audit.list({ ...query, since, workspace: query.workspace.toLowerCase(), visibility });
36}
37
38/** Every entry matching `query`, page by page, up to `EXPORT_LIMIT`. */
39export async function auditAll(viewer: Viewer, query: Omit<AuditQuery, "visibility">): Promise<AuditEntry[] | null> {
40 const entries: AuditEntry[] = [];
41 let before = query.before ?? null;
42 while (entries.length < EXPORT_LIMIT) {
43 const page = await auditPage(viewer, { ...query, before, limit: 500 });
44 if (!page) return null;
45 entries.push(...page.entries);
46 if (!page.next) break;
47 before = page.next;
48 }
49 return entries.slice(0, EXPORT_LIMIT);
50}
51
52/**
53 * What the given runs did, oldest first, for members of the workspace.
54 * Not narrowed to one repository: an attempt on another is what most
55 * needs to be seen.
56 */
57export async function runAudit(viewer: Viewer, owner: string, runIds: string[]): Promise<AuditEntry[]> {
58 if (runIds.length === 0) return [];
59 const page = await auditPage(viewer, {
60 workspace: owner,
61 runIds: runIds.slice(0, 50),
62 limit: 200,
63 }).catch(() => null);
64 return page ? [...page.entries].reverse() : [];
65}