g1t/apps/web/app/lib/guardrails.ts

101 lines3,866 bytesCodeBlame
1import type { GuardrailSettings, RunKind, WorkflowDomain } from "@g1t/contracts";
2
3/** What a level's form field means: inherit, or a choice of its own. */
4export type Tri = "inherit" | "on" | "off";
5
6export function tri(value: boolean | null | undefined): Tri {
7 return value == null ? "inherit" : value ? "on" : "off";
8}
9
10/** Lines of a textarea, trimmed, without blanks or repeats. */
11export function lines(value: FormDataEntryValue | null): string[] {
12 const seen = new Set<string>();
13 for (const line of String(value ?? "").split(/\r?\n/)) {
14 const trimmed = line.trim();
15 if (trimmed) seen.add(trimmed);
16 }
17 return [...seen];
18}
19
20/**
21 * Workflow-only domains, one per line, as
22 * `domain | workflows | environments`: the last two comma-separated, and
23 * either left out or empty for any. `api.cloudflare.com | deploy.yml |
24 * production` lets only deploy.yml's jobs in production reach it.
25 */
26export function workflowDomains(value: FormDataEntryValue | null): WorkflowDomain[] {
27 const list = (text: string | undefined) =>
28 (text ?? "")
29 .split(",")
30 .map((part) => part.trim())
31 .filter(Boolean);
32 const out: WorkflowDomain[] = [];
33 for (const line of lines(value)) {
34 const [domain, workflows, environments] = line.split("|").map((part) => part.trim());
35 if (!domain) continue;
36 out.push({ domain, workflows: list(workflows), environments: list(environments) });
37 }
38 return out;
39}
40
41/** A workflow-only domain as a line of the form's field. */
42export function workflowDomainLine(entry: WorkflowDomain): string {
43 const parts = [entry.domain, entry.workflows.join(", "), entry.environments.join(", ")];
44 while (parts.length > 1 && !parts[parts.length - 1]) parts.pop();
45 return parts.join(" | ");
46}
47
48/** A number from a field; empty means inherit. Not a number is kept, to be refused. */
49function amount(value: FormDataEntryValue | null): number | null {
50 const text = String(value ?? "").trim().replace(/^\$/, "");
51 if (!text) return null;
52 const number = Number(text);
53 return Number.isFinite(number) ? number : Number.NaN;
54}
55
56/** What one level's form says, as the settings that level keeps. */
57export function settingsFromForm(
58 form: FormData,
59 catalog: { registries: string[]; rules: string[]; kinds: readonly RunKind[] },
60): GuardrailSettings {
61 const choice = (name: string): boolean | null => {
62 const value = form.get(name);
63 return value === "on" ? true : value === "off" ? false : null;
64 };
65 const rules: Record<string, boolean> = {};
66 for (const id of catalog.rules) {
67 const on = choice(`rule:${id}`);
68 if (on != null) rules[id] = on;
69 }
70 const minutes: Record<string, number> = {};
71 for (const kind of catalog.kinds) {
72 const cap = amount(form.get(`minutes:${kind}`));
73 if (cap != null) minutes[kind] = Number.isNaN(cap) ? 0 : Math.trunc(cap);
74 }
75 const budget = amount(form.get("budgetUsd"));
76 return {
77 restrictNetwork: choice("restrictNetwork"),
78 registries:
79 form.get("registries") === "custom"
80 ? catalog.registries.filter((id) => form.get(`registry:${id}`) === "on")
81 : null,
82 domains: lines(form.get("domains")),
83 workflowDomains: workflowDomains(form.get("workflowDomains")),
84 rules,
85 deny: lines(form.get("deny")),
86 // Not a number is sent as one the service refuses, with why.
87 budgetUsd: budget == null ? null : Number.isNaN(budget) ? -1 : budget,
88 minutes,
89 };
90}
91
92/** A cost in dollars, or "no cap". */
93export function formatCap(usd: number | null | undefined): string {
94 return usd == null ? "no cap" : `$${usd.toFixed(2)}`;
95}
96
97/** How full a cap is, from 0 to 1, or null with no cap. */
98export function capShare(used: number | null | undefined, cap: number | null | undefined): number | null {
99 if (cap == null || cap <= 0 || used == null) return null;
100 return Math.min(1, Math.max(0, used / cap));
101}