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

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