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.
This commit is contained in:
2026-10-30 07:49:31 +00:00
parent 69b6efea9b
commit d53c22b641
9 changed files with 1171 additions and 12 deletions
+31 -2
View File
@@ -3,8 +3,37 @@
* they test without a database - same reasoning as `dispatch/job_status.ts`.
*/
/** Vendors a gateway can actually drive today. Everything else is found by discovery but not yet controllable. */
export const PROVISIONABLE_VENDORS = ["bambu"] as const;
/**
* 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