ink-signature: pen-plotter pipeline for original wet-ink CMS signatures
The Standard no-login CMS path needs an ORIGINAL ink signature on paper (CMS-10114: 'Stamped, faxed or copied signatures will not be accepted'). This adds a pipeline to redraw the provider's own captured strokes in real ink with a pen on a CR-10 V2 (or any Marlin/GRBL machine) — original, in ink, never copied. - migration 090: esign_records.signature_vector (JSONB stroke paths, 0..1). - signing page now captures normalized stroke paths alongside the PNG; API stores a size-bounded vector for drawn signatures. - ink_signature_plotter.py (hardware-independent): fit strokes to the signature anchor box, PDF-pt -> bed-mm via jig offset, emit Marlin/GRBL G-code (Z pen or M280 servo/BLTouch), SVG toolpath preview, and render_signature_on_pdf (a digital twin that proves the toolpath lands on the cert line). Gated serial sender (dry_run default). - ink_signature_cli.py: end-to-end load-record -> gcode+preview, --test-box jig calibration, --plot to stream over USB. - Corrected CMS-10114 signature anchor to sit inside the Section 4A signing cell (above the bottom rule, below the label). - docs/ink-signature-plotter.md documents the CR-10 retrofit + interpretive risk. Tests: test_ink_signature.py 30/30, test_cms10114.py 27/27, test_paper_batch.py 15/15, API tsc clean, Astro build 58 pages.
This commit is contained in:
parent
e6a630ada1
commit
b0a8563a93
8 changed files with 994 additions and 19 deletions
|
|
@ -117,7 +117,7 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
const { order_id: orderNumber, order_type: documentType, email } = req.portalAuth!;
|
||||
|
||||
const { signature, agreed_at, user_agent } = req.body as {
|
||||
signature?: { type: "drawn" | "typed"; image_b64?: string; name?: string };
|
||||
signature?: { type: "drawn" | "typed"; image_b64?: string; name?: string; vector?: any };
|
||||
agreed_at?: string;
|
||||
user_agent?: string;
|
||||
};
|
||||
|
|
@ -174,6 +174,23 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
? signature.image_b64!.replace(/^data:image\/png;base64,/, "")
|
||||
: signature.name!.trim();
|
||||
|
||||
// Sanitize the optional vector (stroke paths) — bound the size so a hostile
|
||||
// client can't store an enormous JSON blob. Only kept for drawn signatures.
|
||||
let sigVector: string | null = null;
|
||||
if (signature.type === "drawn" && signature.vector && Array.isArray(signature.vector.strokes)) {
|
||||
const v = signature.vector;
|
||||
const strokeCount = v.strokes.length;
|
||||
const pointCount = v.strokes.reduce((n: number, s: any) => n + (Array.isArray(s) ? s.length : 0), 0);
|
||||
if (strokeCount > 0 && strokeCount <= 500 && pointCount > 0 && pointCount <= 20000) {
|
||||
sigVector = JSON.stringify({
|
||||
v: 1,
|
||||
w: Number(v.w) || 0,
|
||||
h: Number(v.h) || 0,
|
||||
strokes: v.strokes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const clientIp = (req as any).clientIp || req.ip || "";
|
||||
const signedAt = new Date().toISOString();
|
||||
|
||||
|
|
@ -182,16 +199,18 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
SET status = 'signed',
|
||||
signature_type = $1,
|
||||
signature_data = $2,
|
||||
signer_email = $3,
|
||||
signer_ip = $4,
|
||||
signer_user_agent = $5,
|
||||
signed_at = $6,
|
||||
perjury_agreed = $7,
|
||||
signature_vector = $3,
|
||||
signer_email = $4,
|
||||
signer_ip = $5,
|
||||
signer_user_agent = $6,
|
||||
signed_at = $7,
|
||||
perjury_agreed = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $8`,
|
||||
WHERE id = $9`,
|
||||
[
|
||||
signature.type,
|
||||
sigData,
|
||||
sigVector,
|
||||
email,
|
||||
clientIp,
|
||||
user_agent || "",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue