Files
continuum-backend/src/provision/plan.ts
T
octoturge d53c22b641 Add Klipper, OctoPrint and PrusaLink printers from the API
POST /farms/:id/printers now takes any of the four vendors the gateway can
drive. Credentials depend on the vendor: Bambu needs an access code and
serial, OctoPrint and PrusaLink an API key, Klipper nothing. A missing one
is a 422 naming what is needed. The API key is relayed to the gateway and
kept out of logs and errors like the access code.

The printer_vendor enum gains octoprint (migration 0006); PrusaLink is
stored as prusa, the name the enum already had.

scripts/e2e-vendors.sh adds each kind through the API against a fake REST
printer (scripts/fake-http-printer.ts) and runs a job through the real
scheduler to completion. The fake shares its author's reading of the docs
with the driver, so this proves the plumbing, not the protocols.
2026-10-30 07:49:31 +00:00

70 lines
2.9 KiB
TypeScript

/**
* 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<ProvisionableVendor, { accessCode?: true; serialNumber?: true; apiKey?: true; dbVendor: "bambu" | "klipper" | "octoprint" | "prusa" }> = {
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("<hidden>");
}
return message;
}