Add a printer through the gateway without the cloud keeping its credential

POST /farms/:id/printers (owner) is the console's add-printer call. It needs
a connected gateway and relays the printer's access code to it in a single
provision_printer frame; the gateway connects to the printer to prove the
code, saves it in its own database, and only then does the cloud create the
printer row. A refused or timed-out add leaves no row, and a row insert that
fails after the gateway said yes tells the gateway to forget the printer.

The access code is held in the request and the one frame and nowhere else:
not in any table, not in a log line, not in a response or an error (the
gateway's error text is scrubbed of it as a second line of defence).
Deleting a printer now also tells the gateway to forget it and its stored
credential, best effort, so an unplugged gateway cannot block an owner.
Only Bambu is accepted, because it is the only vendor with a working
driver; the rest are found by discovery but cannot yet be driven.

e2e-provision.sh drives a stand-in gateway over the real socket and, after
adding a printer, searches every table of the database and the backend's
log for the code (checked to fail when the code is logged).
This commit is contained in:
2026-10-29 15:06:10 +00:00
parent 847f7cb686
commit 46bfecf3e7
7 changed files with 546 additions and 6 deletions
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test";
import { gatewayIdFor, safeGatewayError } from "../src/provision/plan";
describe("gatewayIdFor", () => {
const accepted = /^[A-Za-z0-9_-]{1,64}$/;
test("makes a readable id the gateway will accept", () => {
expect(gatewayIdFor("Bench P1P", () => 0)).toBe("bench-p1p-0000");
expect(gatewayIdFor("Bench P1P", () => 0.999999)).toMatch(accepted);
});
test("survives names with nothing usable in them", () => {
expect(gatewayIdFor("***", () => 0.5)).toMatch(/^printer-[0-9a-f]{4}$/);
expect(gatewayIdFor("Принтер", () => 0.5)).toMatch(accepted);
});
test("never exceeds what the gateway accepts, whatever the name", () => {
expect(gatewayIdFor("x".repeat(500))).toMatch(accepted);
expect(gatewayIdFor("../../etc/passwd")).toMatch(accepted);
});
test("two printers with the same name get different ids", () => {
const ids = new Set(Array.from({ length: 50 }, () => gatewayIdFor("Same name")));
expect(ids.size).toBeGreaterThan(30);
});
});
describe("safeGatewayError", () => {
test("passes an ordinary message through", () => {
expect(safeGatewayError("could not connect to the printer", ["12345678"])).toBe(
"could not connect to the printer",
);
});
test("removes a credential that somehow got into the message", () => {
expect(safeGatewayError("bad login 12345678 for host", ["12345678"])).toBe("bad login <hidden> for host");
});
test("has an answer when the gateway gave none", () => {
expect(safeGatewayError(undefined, [])).toBe("the gateway did not say why");
});
});