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.
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 1 | /** |
| 2 | * What a workspace without the g1t plan may keep in files put in its pages | |
| 3 | * and artifacts (images, attachments): `FREE_DOC_FILES_BYTES` (1 GiB, a | |
| 4 | * wrangler var; `0` is no cap). Past it, a new upload is refused with a | |
| 5 | * message saying so; nothing already kept is touched. Billing is asked | |
| 6 | * only once the workspace is past the cap, and when it does not answer the | |
| 7 | * upload goes ahead. | |
| 8 | */ | |
| 9 | import type { ServiceBinding } from "@g1t/contracts"; | |
| 10 | ||
| 11 | export const DEFAULT_FREE_DOC_FILES_BYTES = 1024 * 1024 * 1024; | |
| 12 | ||
| 13 | export type QuotaEnv = { BILLING?: ServiceBinding; FREE_DOC_FILES_BYTES?: string }; | |
| 14 | ||
| 15 | /** The cap from its var: the default when unset or not a whole number; 0 is none. */ | |
| 16 | export function freeFilesCap(raw: string | undefined): number { | |
| 17 | const value = Number((raw ?? "").trim()); | |
| 18 | return raw !== undefined && raw.trim() !== "" && Number.isInteger(value) && value >= 0 ? value : DEFAULT_FREE_DOC_FILES_BYTES; | |
| 19 | } | |
| 20 | ||
| 21 | /** `1 GB`, `1.5 GB`, `300 MB`. */ | |
| 22 | export function shownBytes(bytes: number): string { | |
| 23 | const gb = 1024 * 1024 * 1024; | |
| 24 | if (bytes >= gb) { | |
| 25 | const tenths = Math.ceil((bytes * 10) / gb); | |
| 26 | return tenths % 10 === 0 ? `${tenths / 10} GB` : `${(tenths / 10).toFixed(1)} GB`; | |
| 27 | } | |
| 28 | return `${Math.ceil(bytes / (1024 * 1024))} MB`; | |
| 29 | } | |
| 30 | ||
| 31 | /** What a free workspace is told when `adding` would take it past `cap`; null while it fits. */ | |
| 32 | export function filesRefusal(held: number, adding: number, cap: number): string | null { | |
| 33 | if (cap <= 0 || held + adding <= cap) return null; | |
| 34 | return `This workspace's files hold ${shownBytes(held)} of the ${shownBytes(cap)} a workspace on the free plan keeps, so this file is not kept. Delete files you no longer need, or start the g1t plan, which has no such cap.`; | |
| 35 | } | |
| 36 | ||
| 37 | /** Why an upload of `adding` bytes to `workspace` is refused, or null. */ | |
| 38 | export async function uploadRefused( | |
| 39 | db: D1Database, | |
| 40 | env: QuotaEnv, | |
| 41 | workspace: { id: string; slug: string }, | |
| 42 | adding: number, | |
| 43 | ): Promise<string | null> { | |
| 44 | const cap = freeFilesCap(env.FREE_DOC_FILES_BYTES); | |
| 45 | if (cap <= 0) return null; | |
| 46 | const row = await db | |
| 47 | .prepare( | |
| 48 | `SELECT (SELECT COALESCE(SUM(bytes), 0) FROM files WHERE workspace_id = ?1) | |
| 49 | + (SELECT COALESCE(SUM(bytes), 0) FROM folio_files WHERE workspace_id = ?1) AS bytes`, | |
| 50 | ) | |
| 51 | .bind(workspace.id) | |
| 52 | .first<{ bytes: number }>(); | |
| 53 | const why = filesRefusal(row?.bytes ?? 0, adding, cap); | |
| 54 | if (!why || !env.BILLING) return null; | |
| 55 | try { | |
| 56 | // Loaded here, so the rules above test under Node without the contracts' runtime. | |
| 57 | const { billingClient } = await import("@g1t/contracts"); | |
| 58 | const free = await billingClient(env.BILLING).freeWorkspaces([workspace.slug]); | |
| 59 | return free.map((slug) => slug.toLowerCase()).includes(workspace.slug.toLowerCase()) ? why : null; | |
| 60 | } catch (error) { | |
| 61 | console.error("artifacts: billing did not say whether the workspace is free", workspace.slug, error); | |
| 62 | return null; | |
| 63 | } | |
| 64 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.