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

66 lines2,626 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import { env } from "cloudflare:workers";
2
3import { type AuditEntry, type AuditQuery, type Viewer, auditClient } from "@g1t/contracts";
4
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put5import { retainedSince, visibilityFor } from "./audit";
Fast pages, required checks on the branch, self-hosted runners, honest incidents6import { instrumented } from "./perf.server";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put7import { billing } from "./services.server";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API8import { roleIn } from "./session.server";
9
10/** The audit log, which the events service keeps. */
Fast pages, required checks on the branch, self-hosted runners, honest incidents11export const audit = auditClient(instrumented("events", env.EVENTS));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put13/**
Audit logs are kept by plan: a week on free, 90 days on the plan, and what staff set for an account in sudo14 * How many days of the workspace's log are kept: 7 for a free workspace,
15 * 90 on the plan, or what g1t staff set for its account. Null when billing
16 * cannot say, and then nothing is held back.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put17 */
18export async function auditRetention(workspace: string): Promise<number | null> {
19 const found = await billing.entitlements(workspace.toLowerCase()).catch(() => null);
20 return found?.auditRetentionDays ?? null;
21}
22
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API23/** The most rows one export writes. */
24export const EXPORT_LIMIT = 10_000;
25
26/**
27 * Entries of a workspace's log the viewer may see, or null when they may
28 * see none of it.
29 */
30export async function auditPage(viewer: Viewer, query: Omit<AuditQuery, "visibility">) {
31 const visibility = viewer ? visibilityFor(roleIn(viewer, query.workspace), viewer.username) : null;
32 if (!visibility) return null;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33 // Reads and exports go back only as far as the workspace's plan keeps.
34 const days = await auditRetention(query.workspace);
35 const since = days == null ? query.since : retainedSince(query.since, days);
36 return audit.list({ ...query, since, workspace: query.workspace.toLowerCase(), visibility });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API37}
38
39/** Every entry matching `query`, page by page, up to `EXPORT_LIMIT`. */
40export async function auditAll(viewer: Viewer, query: Omit<AuditQuery, "visibility">): Promise<AuditEntry[] | null> {
41 const entries: AuditEntry[] = [];
42 let before = query.before ?? null;
43 while (entries.length < EXPORT_LIMIT) {
44 const page = await auditPage(viewer, { ...query, before, limit: 500 });
45 if (!page) return null;
46 entries.push(...page.entries);
47 if (!page.next) break;
48 before = page.next;
49 }
50 return entries.slice(0, EXPORT_LIMIT);
51}
52
53/**
54 * What the given runs did, oldest first, for members of the workspace.
55 * Not narrowed to one repository: an attempt on another is what most
56 * needs to be seen.
57 */
58export async function runAudit(viewer: Viewer, owner: string, runIds: string[]): Promise<AuditEntry[]> {
59 if (runIds.length === 0) return [];
60 const page = await auditPage(viewer, {
61 workspace: owner,
62 runIds: runIds.slice(0, 50),
63 limit: 200,
64 }).catch(() => null);
65 return page ? [...page.entries].reverse() : [];
66}