| 1 | import type { GuardrailSettings, RunKind, WorkflowDomain } from "@g1t/contracts"; |
| 2 | |
| 3 | /** What a level's form field means: inherit, or a choice of its own. */ |
| 4 | export type Tri = "inherit" | "on" | "off"; |
| 5 | |
| 6 | export 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. */ |
| 11 | export 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 | */ |
| 26 | export 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. */ |
| 42 | export 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. */ |
| 49 | function 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. */ |
| 57 | export 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". */ |
| 93 | export 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. */ |
| 98 | export 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 | } |