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