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.
70 lines
2.9 KiB
TypeScript
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;
|
|
}
|