/** * Pure helpers for adding a printer through a gateway, with no imports so * they test without a database - same reasoning as `dispatch/job_status.ts`. */ /** * Vendors a gateway can drive, by the name the gateway knows them as, and * what each needs to be told. `prusa_link` is stored as `prusa` and * `octoprint` as itself - the database enum predates the gateway's names. * * Bambu is verified against real printers. The other three are written from * their public APIs and tested against mock servers only. */ export const PROVISIONABLE_VENDORS = ["bambu", "klipper", "octoprint", "prusa_link"] as const; export type ProvisionableVendor = (typeof PROVISIONABLE_VENDORS)[number]; const NEEDS: Record = { bambu: { accessCode: true, serialNumber: true, dbVendor: "bambu" }, klipper: { dbVendor: "klipper" }, octoprint: { apiKey: true, dbVendor: "octoprint" }, prusa_link: { apiKey: true, dbVendor: "prusa" }, }; export const dbVendorFor = (vendor: ProvisionableVendor) => NEEDS[vendor].dbVendor; /** The first credential this vendor needs that was not supplied, named for the operator. */ export function missingCredential( vendor: ProvisionableVendor, given: { accessCode?: string; serialNumber?: string; apiKey?: string }, ): string | undefined { const needs = NEEDS[vendor]; if (needs.accessCode && !given.accessCode?.trim()) return "an access code"; if (needs.serialNumber && !given.serialNumber?.trim()) return "a serial number"; if (needs.apiKey && !given.apiKey?.trim()) return "an API key"; return undefined; } /** * The id the gateway will know this printer by, chosen here so telemetry * arrives under an id the cloud already holds. A readable slug of the name * plus a random suffix: readable because it shows up in the gateway's logs * and certificate-pin file names, random because two printers may share a * name. Constrained to what the gateway accepts (`^[A-Za-z0-9_-]{1,64}$`). */ export function gatewayIdFor(name: string, random: () => number = Math.random): string { const slug = name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 40) || "printer"; const suffix = Math.floor(random() * 0xffff) .toString(16) .padStart(4, "0"); return `${slug}-${suffix}`; } /** * What to tell the operator when the gateway says no, without ever repeating * the credential back. The gateway's messages do not contain it by design, * and this is the second line of that defence: anything that does is dropped. */ export function safeGatewayError(error: string | undefined, secrets: string[]): string { let message = error ?? "the gateway did not say why"; for (const secret of secrets) { if (secret) message = message.split(secret).join(""); } return message; }