esign: ink-reproduction consent gate + patent-risk research
Consent gate (the legal linchpin from the wet-signature memo): - migration 092 adds ink_consent/ink_consent_at/ink_consent_text to esign_records - extract pure, unit-tested gate logic into esign-ink-consent.ts (DRY single source for route + signing page): isInkReproduction / inkConsentRequired / inkConsentSatisfied + verbatim client-safe INK_CONSENT_TEXT - portal-esign-generic.ts: GET surfaces ink_reproduction + consent text; POST gates DRAWN signatures on ink-path docs on explicit consent, stores it - signing page locks the signature block until consent is checked (drawn only) - npi_provider marks cms855/cms10114 esign metadata ink_reproduction=true - 33 unit checks: gate truth table + consent text omits all internal mechanics (plotter/machine/CMS/MAC/etc) and keeps required legal reassurances Patent-risk memo (docs/legal/patent-risk-mechanical-wet-signature.md): - prior-art-dated risk analysis (autopen 1803/1942, plotters, CNC = public domain => low risk on core concept; e-sign workflow space litigious) - firsthand recent-grant sweep (1.58M USPTO grants 2021-2025, queried via DuckDB): ZERO patents on machine-applies-signature-in-ink; e-sign players hold only electronic-workflow patents. Not an FTO; flags where attorney search is needed
This commit is contained in:
parent
f8d2a7f01f
commit
a4bad723bc
7 changed files with 452 additions and 5 deletions
28
api/migrations/092_esign_ink_consent.sql
Normal file
28
api/migrations/092_esign_ink_consent.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- 092: Ink-reproduction consent on signature records.
|
||||
--
|
||||
-- The Standard (no-login) CMS filing path reproduces the signer's OWN captured
|
||||
-- signature strokes in real ink on the printed form (pen plotter) so the mailed
|
||||
-- application carries an original ink signature. Per the legal-risk research
|
||||
-- (docs/legal/remote-mechanical-wet-signature-precedent.md), the linchpin that
|
||||
-- keeps this on the valid side of the forgery/agency line is an EXPLICIT,
|
||||
-- per-document authorization from the signer to reproduce their signature in ink
|
||||
-- on this specific document.
|
||||
--
|
||||
-- These columns capture that consent at signing time, alongside the existing
|
||||
-- perjury attestation. They are only meaningful for drawn signatures on ink-path
|
||||
-- documents (metadata.ink_reproduction = true); other docs leave them false/NULL.
|
||||
--
|
||||
-- Idempotent.
|
||||
|
||||
ALTER TABLE esign_records
|
||||
ADD COLUMN IF NOT EXISTS ink_consent BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS ink_consent_text TEXT;
|
||||
|
||||
COMMENT ON COLUMN esign_records.ink_consent IS
|
||||
'TRUE when the signer expressly authorized reproducing their drawn signature '
|
||||
'in ink on this document (pen-plotter path). Captured at signing time.';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_at IS
|
||||
'When the ink-reproduction consent was given (signer-side timestamp).';
|
||||
COMMENT ON COLUMN esign_records.ink_consent_text IS
|
||||
'Verbatim consent language the signer agreed to (for the audit trail).';
|
||||
59
api/src/routes/esign-ink-consent.ts
Normal file
59
api/src/routes/esign-ink-consent.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Pure helpers for the ink-reproduction consent gate (DB-free, unit-tested).
|
||||
*
|
||||
* The Standard (no-login) CMS filing path reproduces the signer's OWN captured
|
||||
* signature strokes in real ink on the printed form (pen plotter). The legal
|
||||
* linchpin (docs/legal/remote-mechanical-wet-signature-precedent.md) is an
|
||||
* EXPLICIT, per-document authorization to reproduce the signature in ink. These
|
||||
* helpers decide when that consent is required and whether it was satisfied, so
|
||||
* the route and the signing page agree on one source of truth.
|
||||
*/
|
||||
|
||||
/** Verbatim consent the signer must agree to before drawing an ink-path signature.
|
||||
*
|
||||
* Client-safe (no internal mechanics) but legally explicit: authorizes Performance
|
||||
* West to reproduce THIS signer's OWN drawn signature in ink ONE TIME on THIS
|
||||
* single document. The "only once / this one form / not reused" language reassures
|
||||
* the signer the signature is not stored for reuse. Stored verbatim with the
|
||||
* signature for the audit trail.
|
||||
*/
|
||||
export const INK_CONSENT_TEXT =
|
||||
"I understand this filing must be submitted on the official paper form with an " +
|
||||
"original ink signature. I authorize Performance West Inc. to reproduce my own " +
|
||||
"signature, exactly as I draw it below, in ink one time on this single form, and " +
|
||||
"to submit it on my behalf. My signature will be used solely to complete this " +
|
||||
"filing and will not be reused for any other document or purpose. The signature " +
|
||||
"applied will be my own signature, made with my authorization and with the " +
|
||||
"intent to sign this document.";
|
||||
|
||||
/** Is this document on the ink-reproduction path? (metadata.ink_reproduction). */
|
||||
export function isInkReproduction(documentMetadata: unknown): boolean {
|
||||
return !!documentMetadata
|
||||
&& typeof documentMetadata === "object"
|
||||
&& (documentMetadata as Record<string, unknown>).ink_reproduction === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this signing attempt REQUIRE the ink-reproduction consent?
|
||||
* Only a DRAWN signature on an ink-path document needs it (a typed signature is
|
||||
* not reproduced as the signer's own hand, so it is exempt).
|
||||
*/
|
||||
export function inkConsentRequired(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
): boolean {
|
||||
return isInkReproduction(documentMetadata) && signatureType === "drawn";
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the consent satisfied for this attempt? True when not required, or when
|
||||
* required and the signer explicitly gave it (ink_consent === true).
|
||||
*/
|
||||
export function inkConsentSatisfied(
|
||||
documentMetadata: unknown,
|
||||
signatureType: "drawn" | "typed" | string | undefined,
|
||||
inkConsent: unknown,
|
||||
): boolean {
|
||||
if (!inkConsentRequired(documentMetadata, signatureType)) return true;
|
||||
return inkConsent === true;
|
||||
}
|
||||
|
|
@ -22,6 +22,11 @@
|
|||
import { Router, type Request, type Response } from "express";
|
||||
import { pool } from "../db.js";
|
||||
import { requirePortalAuth } from "../middleware/portalAuth.js";
|
||||
import {
|
||||
INK_CONSENT_TEXT,
|
||||
isInkReproduction,
|
||||
inkConsentSatisfied,
|
||||
} from "./esign-ink-consent.js";
|
||||
|
||||
const router = Router();
|
||||
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
|
||||
|
|
@ -93,12 +98,21 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
// Pull order number for subtitle (e.g. "CO-ABC12345")
|
||||
const metadata = rec.document_metadata || {};
|
||||
|
||||
// Ink-reproduction path: when this document's signature will be reproduced in
|
||||
// real ink on the printed form (pen-plotter / Standard CMS filing path), the
|
||||
// signing page must collect an explicit, per-document consent to reproduce the
|
||||
// drawn signature in ink BEFORE capturing it. See
|
||||
// docs/legal/remote-mechanical-wet-signature-precedent.md.
|
||||
const inkReproduction = isInkReproduction(metadata);
|
||||
|
||||
res.json({
|
||||
document_title: rec.document_title,
|
||||
entity_name: rec.entity_name,
|
||||
order_number: rec.order_number,
|
||||
document_url: documentUrl,
|
||||
requires_perjury: rec.requires_perjury || false,
|
||||
ink_reproduction: inkReproduction,
|
||||
ink_consent_text: inkReproduction ? INK_CONSENT_TEXT : null,
|
||||
metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
@ -116,10 +130,11 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res: Response) => {
|
||||
const { order_id: orderNumber, order_type: documentType, email } = req.portalAuth!;
|
||||
|
||||
const { signature, agreed_at, user_agent } = req.body as {
|
||||
const { signature, agreed_at, user_agent, ink_consent } = req.body as {
|
||||
signature?: { type: "drawn" | "typed"; image_b64?: string; name?: string; vector?: any };
|
||||
agreed_at?: string;
|
||||
user_agent?: string;
|
||||
ink_consent?: boolean;
|
||||
};
|
||||
|
||||
// Validate signature
|
||||
|
|
@ -146,7 +161,8 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
try {
|
||||
// Find the pending record
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, order_number, document_type, status, requires_perjury
|
||||
`SELECT id, order_number, document_type, status, requires_perjury,
|
||||
document_metadata, signed_at
|
||||
FROM esign_records
|
||||
WHERE order_number = $1
|
||||
AND document_type = $2
|
||||
|
|
@ -169,6 +185,19 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
return;
|
||||
}
|
||||
|
||||
// Ink-reproduction gate: if this document's signature will be reproduced in
|
||||
// ink on the official paper form, a drawn signature REQUIRES the explicit
|
||||
// per-document ink-reproduction consent (the legal linchpin — see
|
||||
// docs/legal/remote-mechanical-wet-signature-precedent.md). A typed signature
|
||||
// is not reproduced as the signer's own hand, so it does not need this gate.
|
||||
const inkReproduction = isInkReproduction(rec.document_metadata);
|
||||
if (!inkConsentSatisfied(rec.document_metadata, signature.type, ink_consent)) {
|
||||
res.status(400).json({
|
||||
error: "Please confirm the authorization to use your signature on the official form.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the signature
|
||||
const sigData = signature.type === "drawn"
|
||||
? signature.image_b64!.replace(/^data:image\/png;base64,/, "")
|
||||
|
|
@ -194,6 +223,10 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
const clientIp = (req as any).clientIp || req.ip || "";
|
||||
const signedAt = new Date().toISOString();
|
||||
|
||||
// Record the ink-reproduction consent (only meaningful when this document is
|
||||
// on the ink-reproduction path and the signer drew their signature).
|
||||
const inkConsentGiven = inkReproduction && signature.type === "drawn" && ink_consent === true;
|
||||
|
||||
await pool.query(
|
||||
`UPDATE esign_records
|
||||
SET status = 'signed',
|
||||
|
|
@ -205,8 +238,11 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
signer_user_agent = $6,
|
||||
signed_at = $7,
|
||||
perjury_agreed = $8,
|
||||
ink_consent = $9,
|
||||
ink_consent_at = $10,
|
||||
ink_consent_text = $11,
|
||||
updated_at = NOW()
|
||||
WHERE id = $9`,
|
||||
WHERE id = $12`,
|
||||
[
|
||||
signature.type,
|
||||
sigData,
|
||||
|
|
@ -216,6 +252,9 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
|||
user_agent || "",
|
||||
signedAt,
|
||||
rec.requires_perjury ? true : false,
|
||||
inkConsentGiven,
|
||||
inkConsentGiven ? signedAt : null,
|
||||
inkConsentGiven ? INK_CONSENT_TEXT : null,
|
||||
rec.id,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
58
api/test/test_esign_ink_consent.ts
Normal file
58
api/test/test_esign_ink_consent.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Unit tests for the ink-reproduction consent gate (pure logic, no DB).
|
||||
* Run: npx tsx api/test/test_esign_ink_consent.ts
|
||||
*/
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
INK_CONSENT_TEXT,
|
||||
isInkReproduction,
|
||||
inkConsentRequired,
|
||||
inkConsentSatisfied,
|
||||
} from "../src/routes/esign-ink-consent.js";
|
||||
|
||||
let pass = 0;
|
||||
const ok = (name: string, cond: boolean) => {
|
||||
assert.ok(cond, name);
|
||||
pass++;
|
||||
};
|
||||
|
||||
// --- isInkReproduction ---
|
||||
ok("ink meta true", isInkReproduction({ ink_reproduction: true }) === true);
|
||||
ok("ink meta false", isInkReproduction({ ink_reproduction: false }) === false);
|
||||
ok("ink meta missing", isInkReproduction({}) === false);
|
||||
ok("ink meta null", isInkReproduction(null) === false);
|
||||
ok("ink meta undefined", isInkReproduction(undefined) === false);
|
||||
ok("ink meta string-truthy not enough", isInkReproduction({ ink_reproduction: "true" }) === false);
|
||||
|
||||
// --- inkConsentRequired: only DRAWN on ink-path docs ---
|
||||
ok("required: drawn + ink", inkConsentRequired({ ink_reproduction: true }, "drawn") === true);
|
||||
ok("not required: typed + ink", inkConsentRequired({ ink_reproduction: true }, "typed") === false);
|
||||
ok("not required: drawn + non-ink", inkConsentRequired({ ink_reproduction: false }, "drawn") === false);
|
||||
ok("not required: drawn + no meta", inkConsentRequired({}, "drawn") === false);
|
||||
ok("not required: undefined type", inkConsentRequired({ ink_reproduction: true }, undefined) === false);
|
||||
|
||||
// --- inkConsentSatisfied ---
|
||||
// exempt cases (consent not required) are always satisfied regardless of flag
|
||||
ok("satisfied: typed exempt (no consent)", inkConsentSatisfied({ ink_reproduction: true }, "typed", undefined) === true);
|
||||
ok("satisfied: non-ink drawn exempt", inkConsentSatisfied({ ink_reproduction: false }, "drawn", undefined) === true);
|
||||
ok("satisfied: no-meta drawn exempt", inkConsentSatisfied({}, "drawn", false) === true);
|
||||
// required cases: must have ink_consent === true
|
||||
ok("blocked: drawn+ink, no consent", inkConsentSatisfied({ ink_reproduction: true }, "drawn", undefined) === false);
|
||||
ok("blocked: drawn+ink, consent false", inkConsentSatisfied({ ink_reproduction: true }, "drawn", false) === false);
|
||||
ok("blocked: drawn+ink, consent truthy-but-not-true", inkConsentSatisfied({ ink_reproduction: true }, "drawn", "true") === false);
|
||||
ok("blocked: drawn+ink, consent 1 (not strict true)", inkConsentSatisfied({ ink_reproduction: true }, "drawn", 1) === false);
|
||||
ok("allowed: drawn+ink, consent true", inkConsentSatisfied({ ink_reproduction: true }, "drawn", true) === true);
|
||||
|
||||
// --- consent text: client-safe (no internal mechanics) ---
|
||||
const banned = ["plotter", "machine", "CMS", "855", "10114", "MAC", "Baltimore", "PO Box", "robot"];
|
||||
for (const w of banned) {
|
||||
ok(`consent text omits "${w}"`, !INK_CONSENT_TEXT.toLowerCase().includes(w.toLowerCase()));
|
||||
}
|
||||
// --- consent text: legally required reassurances present ---
|
||||
ok("consent says 'ink one time'", INK_CONSENT_TEXT.includes("in ink one time"));
|
||||
ok("consent says 'single form'", INK_CONSENT_TEXT.includes("single form"));
|
||||
ok("consent says 'not be reused'", INK_CONSENT_TEXT.includes("will not be reused"));
|
||||
ok("consent says 'my own signature'", INK_CONSENT_TEXT.includes("my own signature"));
|
||||
ok("consent says 'intent to sign'", INK_CONSENT_TEXT.includes("intent to sign"));
|
||||
|
||||
console.log(`\nesign ink-consent: ${pass} checks passed`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue