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 { Router, type Request, type Response } from "express";
|
||||||
import { pool } from "../db.js";
|
import { pool } from "../db.js";
|
||||||
import { requirePortalAuth } from "../middleware/portalAuth.js";
|
import { requirePortalAuth } from "../middleware/portalAuth.js";
|
||||||
|
import {
|
||||||
|
INK_CONSENT_TEXT,
|
||||||
|
isInkReproduction,
|
||||||
|
inkConsentSatisfied,
|
||||||
|
} from "./esign-ink-consent.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
|
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")
|
// Pull order number for subtitle (e.g. "CO-ABC12345")
|
||||||
const metadata = rec.document_metadata || {};
|
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({
|
res.json({
|
||||||
document_title: rec.document_title,
|
document_title: rec.document_title,
|
||||||
entity_name: rec.entity_name,
|
entity_name: rec.entity_name,
|
||||||
order_number: rec.order_number,
|
order_number: rec.order_number,
|
||||||
document_url: documentUrl,
|
document_url: documentUrl,
|
||||||
requires_perjury: rec.requires_perjury || false,
|
requires_perjury: rec.requires_perjury || false,
|
||||||
|
ink_reproduction: inkReproduction,
|
||||||
|
ink_consent_text: inkReproduction ? INK_CONSENT_TEXT : null,
|
||||||
metadata,
|
metadata,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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) => {
|
router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res: Response) => {
|
||||||
const { order_id: orderNumber, order_type: documentType, email } = req.portalAuth!;
|
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 };
|
signature?: { type: "drawn" | "typed"; image_b64?: string; name?: string; vector?: any };
|
||||||
agreed_at?: string;
|
agreed_at?: string;
|
||||||
user_agent?: string;
|
user_agent?: string;
|
||||||
|
ink_consent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate signature
|
// Validate signature
|
||||||
|
|
@ -146,7 +161,8 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
||||||
try {
|
try {
|
||||||
// Find the pending record
|
// Find the pending record
|
||||||
const { rows } = await pool.query(
|
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
|
FROM esign_records
|
||||||
WHERE order_number = $1
|
WHERE order_number = $1
|
||||||
AND document_type = $2
|
AND document_type = $2
|
||||||
|
|
@ -169,6 +185,19 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
||||||
return;
|
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
|
// Store the signature
|
||||||
const sigData = signature.type === "drawn"
|
const sigData = signature.type === "drawn"
|
||||||
? signature.image_b64!.replace(/^data:image\/png;base64,/, "")
|
? 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 clientIp = (req as any).clientIp || req.ip || "";
|
||||||
const signedAt = new Date().toISOString();
|
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(
|
await pool.query(
|
||||||
`UPDATE esign_records
|
`UPDATE esign_records
|
||||||
SET status = 'signed',
|
SET status = 'signed',
|
||||||
|
|
@ -205,8 +238,11 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
||||||
signer_user_agent = $6,
|
signer_user_agent = $6,
|
||||||
signed_at = $7,
|
signed_at = $7,
|
||||||
perjury_agreed = $8,
|
perjury_agreed = $8,
|
||||||
|
ink_consent = $9,
|
||||||
|
ink_consent_at = $10,
|
||||||
|
ink_consent_text = $11,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $9`,
|
WHERE id = $12`,
|
||||||
[
|
[
|
||||||
signature.type,
|
signature.type,
|
||||||
sigData,
|
sigData,
|
||||||
|
|
@ -216,6 +252,9 @@ router.post("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
|
||||||
user_agent || "",
|
user_agent || "",
|
||||||
signedAt,
|
signedAt,
|
||||||
rec.requires_perjury ? true : false,
|
rec.requires_perjury ? true : false,
|
||||||
|
inkConsentGiven,
|
||||||
|
inkConsentGiven ? signedAt : null,
|
||||||
|
inkConsentGiven ? INK_CONSENT_TEXT : null,
|
||||||
rec.id,
|
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`);
|
||||||
214
docs/legal/patent-risk-mechanical-wet-signature.md
Normal file
214
docs/legal/patent-risk-mechanical-wet-signature.md
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
# Patent-risk analysis: remote signature capture + mechanical wet-ink reproduction
|
||||||
|
|
||||||
|
Status: internal patent-RISK research. **Not legal advice and not a freedom-to-
|
||||||
|
operate (FTO) opinion.** A real FTO requires a patent attorney to search live
|
||||||
|
claims and read them against our implementation. This memo identifies where risk
|
||||||
|
is low (mature/expired prior art) vs. where a targeted FTO search is warranted.
|
||||||
|
|
||||||
|
Companion docs: `docs/legal/remote-mechanical-wet-signature-precedent.md`,
|
||||||
|
`docs/plans/remote-wet-signature-products.md`.
|
||||||
|
|
||||||
|
What we do (the accused "system," broken into components):
|
||||||
|
1. Capture a hand-drawn signature in a browser as **vector strokes** (x, y, t).
|
||||||
|
2. Store it + an audit trail (`esign_records`), send a JWT no-login signing link.
|
||||||
|
3. Generate/fill a PDF form; stamp a digital signature image (existing path).
|
||||||
|
4. **Reproduce the captured strokes in real ink on paper with a pen plotter /
|
||||||
|
Line-us arm** (the novel-feeling part).
|
||||||
|
5. Mail/file the document; track fulfillment.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- **Components 1-3 and 5** are ubiquitous, decades-old techniques (signature
|
||||||
|
capture, e-sign workflow, PDF fill, mailing automation). **Patent risk: low**,
|
||||||
|
but the *e-signature workflow* space is litigious (DocuSign, RPost, etc.), so a
|
||||||
|
targeted FTO on any **specific workflow feature** is prudent before scaling.
|
||||||
|
- **Component 4** (drawing a signature in ink with a machine) sits on **very old,
|
||||||
|
expired prior art**: signature-duplicating machines (Hawkins polygraph, 1803),
|
||||||
|
autopens (1930s-1942), pen plotters (1950s-1980s), CNC/G-code motion (1950s+).
|
||||||
|
The *idea* of a machine writing a signature is firmly in the public domain.
|
||||||
|
**Patent risk on the core concept: low.**
|
||||||
|
- The realistic risk is **not** the broad concept but **narrow, recent claims**
|
||||||
|
on a *specific combination/method* (e.g. "capture an e-signature online, then
|
||||||
|
transmit it to a robotic pen to apply wet ink for a regulated filing"). A
|
||||||
|
firsthand title sweep of 1.58M recent grants (2021-2025) found **no such
|
||||||
|
patent**; the e-sign players (DocuSign/Adobe/Silanis) hold only *electronic*
|
||||||
|
workflow patents. This reduces, but does not eliminate, the need for an
|
||||||
|
attorney FTO (which must also cover pre-2021 and pending claims).
|
||||||
|
|
||||||
|
## 1. Signature capture (vector strokes in a browser)
|
||||||
|
|
||||||
|
- **Prior art is overwhelming and old.** Electronic signature pads (Topaz,
|
||||||
|
Wacom), tablet/stylus capture, and HTML5 `<canvas>` signature capture have been
|
||||||
|
standard for 15-25+ years. Capturing (x, y, t) stroke data is basic digitizer
|
||||||
|
technique predating the web.
|
||||||
|
- **Risk: low** for the act of capturing strokes. Any patent broad enough to
|
||||||
|
cover "capture a signature on a touchscreen" would almost certainly be invalid
|
||||||
|
over prior art. Avoid copying a *specific patented capture algorithm* (e.g. a
|
||||||
|
particular biometric/pressure-analysis method) - we use plain stroke capture.
|
||||||
|
|
||||||
|
## 2. E-signature workflow + audit trail + no-login link (JWT)
|
||||||
|
|
||||||
|
- **This is the litigious zone of the three software components.** Companies with
|
||||||
|
e-signature/e-delivery patent portfolios have actively asserted them:
|
||||||
|
- **RPost** (Registered Email / e-signature patents) has a history of
|
||||||
|
licensing/litigation around proof-of-delivery and e-signature.
|
||||||
|
- **DocuSign** holds many patents on signing *workflows*, "envelopes," routing,
|
||||||
|
and tamper-evidence.
|
||||||
|
- **But:** post-*Alice v. CLS Bank* (573 U.S. 208 (2014)), many abstract
|
||||||
|
"do-it-on-a-computer" software/business-method patents are vulnerable to
|
||||||
|
invalidation under 35 U.S.C. § 101. Generic e-sign workflow claims have been
|
||||||
|
narrowed/invalidated.
|
||||||
|
- **Risk: low-moderate.** Our workflow (JWT link, store signature + IP/UA/time,
|
||||||
|
resume a pipeline) is generic and well-trodden. The risk is asserting a
|
||||||
|
*specific patented feature* (e.g. a particular tamper-evidence/seal method, a
|
||||||
|
specific routing/orchestration claim). **Do a targeted FTO before building any
|
||||||
|
distinctive workflow feature** (sequential multi-party routing, a proprietary
|
||||||
|
audit-seal, etc.).
|
||||||
|
|
||||||
|
## 3. PDF generation / form fill / digital stamp
|
||||||
|
|
||||||
|
- **Ubiquitous, mature.** Filling AcroForm fields and overlaying an image onto a
|
||||||
|
PDF is standard library functionality (pypdf, reportlab, PDFBox, etc.) used by
|
||||||
|
millions of applications. **Risk: low.**
|
||||||
|
|
||||||
|
## 4. Mechanical wet-ink reproduction (pen plotter / robotic arm)
|
||||||
|
|
||||||
|
This is the component that *feels* novel but is built on the oldest prior art:
|
||||||
|
|
||||||
|
- **Signature-duplicating machines: 1803.** John Isaac Hawkins patented the
|
||||||
|
"polygraph" pantograph signature copier (used extensively by Jefferson).
|
||||||
|
- **Autopen: 1930s-1942.** The "Robot Pen" (1930s) and De Shazo's commercially
|
||||||
|
successful autopen (1942, Navy order) mass-reproduced signatures in ink. By
|
||||||
|
mid-century there were 500+ autopens in Washington.
|
||||||
|
- **Pen plotters: 1950s-1980s.** X-Y pen plotters (Calcomp 1959; HP 7470A 1980s)
|
||||||
|
draw arbitrary vector paths in ink - exactly our motion problem.
|
||||||
|
- **CNC / G-code motion control: 1950s+.** Numerically-controlled multi-axis
|
||||||
|
motion (which a CR-10/3D printer is) is decades old.
|
||||||
|
- (Sources: Wikipedia "Autopen," retrieved firsthand; well-documented hardware
|
||||||
|
history.)
|
||||||
|
- **Conclusion:** "a machine that applies a person's signature in ink by
|
||||||
|
following a stored path" is **firmly in the public domain.** No one can hold a
|
||||||
|
valid patent on the *concept*. **Risk on the core mechanism: low.**
|
||||||
|
|
||||||
|
Residual risk is only on **narrow modern improvements**, e.g.:
|
||||||
|
- A *specific* signature-reproduction algorithm (pressure/velocity modeling to
|
||||||
|
mimic natural handwriting) that someone recently patented. We use plain
|
||||||
|
fit-to-box stroke replay; avoid copying a specific patented "natural-motion"
|
||||||
|
method without checking.
|
||||||
|
- A *specific combined method claim* tying online e-signature capture to a
|
||||||
|
robotic ink applicator for filings. We found no such patent, but this is the
|
||||||
|
one area an attorney FTO should specifically probe (handwriting-robot startups,
|
||||||
|
e.g. companies in the "robot-written mail" space, may hold method patents on
|
||||||
|
*their* pipelines - those would be narrow to their implementations).
|
||||||
|
|
||||||
|
## 5. Mailing / fulfillment automation
|
||||||
|
|
||||||
|
- **Mature and generic.** Batch mailing, address routing, print-and-mail
|
||||||
|
workflows are old and widely practiced. **Risk: low.** (Note RPost-style
|
||||||
|
proof-of-delivery patents exist; we are not claiming a proprietary e-delivery
|
||||||
|
receipt mechanism.)
|
||||||
|
|
||||||
|
## Recent-grant prior-art sweep (2021-2025, firsthand query)
|
||||||
|
|
||||||
|
To probe the one real risk zone (a recent method patent tying online signature
|
||||||
|
capture to robotic ink application, and e-sign workflow patents our flow might
|
||||||
|
read on), we ran a firsthand title search against a full USPTO grant dataset
|
||||||
|
(PatentsView-derived, **1,575,344 patents granted 2021-06 to 2025-09**). This is
|
||||||
|
a **title-level** sweep of recent grants, **not** a claims-level FTO, and it does
|
||||||
|
not cover pre-2021 art - but it directly tests "did anyone recently patent our
|
||||||
|
specific combination?" The answer is essentially no:
|
||||||
|
|
||||||
|
- **Signature + (ink|pen|stylus|wet) + (robot|machine|apparatus|device): 0
|
||||||
|
relevant hits.** All matches were unrelated (wireless-comms "signatures,"
|
||||||
|
printer ink-color flushing). **No "machine applies a signature in ink" patent
|
||||||
|
appears in 5 years of grants.**
|
||||||
|
- **autopen: 0 hits. signature+plotter: 0. signature+reproduce: 0.**
|
||||||
|
- **"writing/drawing robot": only ornamental DESIGN patents** (D1090657,
|
||||||
|
D1058630 - cover a device's *appearance*, not a signing method) plus an
|
||||||
|
unrelated Yaskawa robot-withdrawal utility patent. Nothing signature-specific.
|
||||||
|
- **"handwriting + machine": only handwriting *extraction* (reading) via ML**
|
||||||
|
(12175799), i.e. the opposite of writing.
|
||||||
|
- **E-signature workflow space is owned by the expected players, all purely
|
||||||
|
electronic (no ink/paper):**
|
||||||
|
- **DocuSign, Inc.** - many recent grants on document models, recipient
|
||||||
|
routing/notification, OCR, contract generation (e.g. 12411896 "Document
|
||||||
|
graph"; RE50043 associating third-party content with online signing).
|
||||||
|
- **Adobe Inc.** - electronic-signature *capture UI* (11132105, 11159328,
|
||||||
|
11750670 "signature collection within an online conference").
|
||||||
|
- **Silanis Technology, Inc.** - 11093652 "Web-based method and system for
|
||||||
|
applying a legally enforceable signature on an electronic document" (the
|
||||||
|
single closest hit by name) - but it is **purely electronic**; it does not
|
||||||
|
apply ink to paper, so it does not read on our mechanical reproduction.
|
||||||
|
- **Tamper/seal:** 11743053 "Electronic signature system and tamper-resistant
|
||||||
|
device" - electronic; relevant only if we built a proprietary tamper-seal
|
||||||
|
(we do not).
|
||||||
|
|
||||||
|
**Interpretation:** the recent-patent landscape is consistent with the prior-art
|
||||||
|
analysis above. The litigious e-sign players hold **electronic-workflow** patents
|
||||||
|
(read them before building any distinctive workflow feature), but **no one holds
|
||||||
|
a recent patent on reproducing a captured signature in ink with a machine for a
|
||||||
|
filing** - our specific, novel-feeling combination. This *reduces* (does not
|
||||||
|
eliminate) the residual risk flagged in section 4.
|
||||||
|
|
||||||
|
**Caveats:** (1) title-only, recent-grant (2021-2025) - misses pre-2021 patents
|
||||||
|
(many e-sign core patents predate 2021) and any in-force patent whose title does
|
||||||
|
not contain these words; (2) does not read claims; (3) does not cover pending
|
||||||
|
applications. **Still not an FTO** - a patent attorney must read live claims
|
||||||
|
(including pre-2021 and pending) against our implementation before launch.
|
||||||
|
|
||||||
|
Method: queried the patent grant parquet directly via DuckDB (title LIKE
|
||||||
|
filters). Dataset coverage and the specific hits above were retrieved firsthand
|
||||||
|
2026-06-07.
|
||||||
|
|
||||||
|
## What would actually create exposure (avoid these without FTO)
|
||||||
|
|
||||||
|
1. Copying a **named competitor's specific feature** (a particular tamper-seal,
|
||||||
|
a specific multi-party routing UX, a specific "robot handwriting realism"
|
||||||
|
algorithm).
|
||||||
|
2. Implementing a **distinctive method** and marketing it as such (the more
|
||||||
|
specific/novel our claim of novelty, the more likely it overlaps a method
|
||||||
|
patent - and the more attractive we are as a target).
|
||||||
|
3. Building the **RON / e-notary** layer without checking notary-tech patents.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. **Now (low cost):** keep the implementation **generic and built on
|
||||||
|
public-domain techniques** (plain stroke capture, standard PDF libs, standard
|
||||||
|
G-code/CNC motion, autopen-class reproduction). This is our best defense - we
|
||||||
|
are practicing decades-old art.
|
||||||
|
2. **Before scaling / raising / public launch:** commission a **freedom-to-operate
|
||||||
|
search** from a patent attorney focused on two questions:
|
||||||
|
(a) any live method patent tying **online e-signature capture → robotic ink
|
||||||
|
application** for documents/filings; (b) any e-signature **workflow** patent
|
||||||
|
our specific features might read on.
|
||||||
|
3. **Defensive posture:** document our **prior-art basis** (this memo + the
|
||||||
|
hardware history) so that if anyone asserts a broad patent, we can argue
|
||||||
|
invalidity/non-infringement. Consider a **defensive publication** of our
|
||||||
|
pipeline (a public description) to create prior art preventing others from
|
||||||
|
patenting the same combination later.
|
||||||
|
4. **Do not patent-troll-bait:** avoid over-claiming novelty in marketing; frame
|
||||||
|
the service as "we file your documents with an original ink signature," not as
|
||||||
|
a patented invention.
|
||||||
|
|
||||||
|
## Honest limits of this memo
|
||||||
|
|
||||||
|
- We ran a **title-level sweep of recent grants (2021-2025)** (see section above)
|
||||||
|
but did **not** run a full **claims-level** search across all years; reading
|
||||||
|
live claims against our code requires a patent attorney. The recent-grant sweep
|
||||||
|
supports the low-risk *concept* assessment but is **not an FTO clearance.**
|
||||||
|
- Patent risk is jurisdiction- and claim-specific and changes as patents issue;
|
||||||
|
re-check before any major launch.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Prior-art hardware history (autopen 1803/1930s/1942, plotters, CNC):
|
||||||
|
Wikipedia "Autopen" (retrieved firsthand 2026-06-07), corroborated by
|
||||||
|
general hardware history.
|
||||||
|
- *Alice Corp. v. CLS Bank Int'l*, 573 U.S. 208 (2014) - § 101 abstract-idea
|
||||||
|
invalidity framework (well-known; confirm citation before reliance).
|
||||||
|
- Litigious-player context (RPost, DocuSign patent portfolios): general industry
|
||||||
|
knowledge plus the firsthand recent-grant sweep above (DocuSign/Adobe/Silanis
|
||||||
|
hits) - confirm specifics with counsel before relying.
|
||||||
|
- Recent-grant prior-art sweep: USPTO patent grant dataset (PatentsView-derived,
|
||||||
|
2021-2025, 1.58M patents), queried firsthand via DuckDB 2026-06-07. Specific
|
||||||
|
patent numbers cited above are real hits from that dataset.
|
||||||
|
|
@ -375,7 +375,7 @@ class _BaseNPIHandler:
|
||||||
customer_name=provider,
|
customer_name=provider,
|
||||||
document_minio_key=document_key,
|
document_minio_key=document_key,
|
||||||
requires_perjury=True,
|
requires_perjury=True,
|
||||||
metadata={"service_slug": self.SERVICE_SLUG, "npi": intake.get("npi", ""), "form_type": form_type},
|
metadata={"service_slug": self.SERVICE_SLUG, "npi": intake.get("npi", ""), "form_type": form_type, "ink_reproduction": True},
|
||||||
expires_hours=21 * 24,
|
expires_hours=21 * 24,
|
||||||
)
|
)
|
||||||
# request_esign does not persist signature anchors; attach them so
|
# request_esign does not persist signature anchors; attach them so
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
#loading{text-align:center;padding:4rem 1rem;color:#64748b}
|
#loading{text-align:center;padding:4rem 1rem;color:#64748b}
|
||||||
#error-screen{display:none;text-align:center;padding:3rem 1rem}
|
#error-screen{display:none;text-align:center;padding:3rem 1rem}
|
||||||
.hidden{display:none}
|
.hidden{display:none}
|
||||||
|
/* Signature block locked until ink-reproduction consent is given */
|
||||||
|
#sig-block.locked{opacity:.45;pointer-events:none;filter:grayscale(.3)}
|
||||||
</style>
|
</style>
|
||||||
<script defer src="https://analytics.performancewest.net/script.js" data-website-id="55250014-ee15-44ac-a1f6-81dabad3fe0f"></script><script defer src="/js/pw-analytics.js"></script></head>
|
<script defer src="https://analytics.performancewest.net/script.js" data-website-id="55250014-ee15-44ac-a1f6-81dabad3fe0f"></script><script defer src="/js/pw-analytics.js"></script></head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -76,6 +78,19 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
<!-- Step 2: Sign -->
|
<!-- Step 2: Sign -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Step 2 — Your Signature</h2>
|
<h2>Step 2 — Your Signature</h2>
|
||||||
|
|
||||||
|
<!-- Ink-reproduction consent gate (shown only for ink-path documents).
|
||||||
|
The signer must authorize use of their signature on the official form
|
||||||
|
BEFORE they can draw it. -->
|
||||||
|
<div id="ink-consent-box" class="hidden" style="padding:1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;margin-bottom:1rem">
|
||||||
|
<p style="font-size:.85rem;color:#1f2937;margin:0 0 .75rem" id="ink-consent-text"></p>
|
||||||
|
<label style="display:flex;gap:.6rem;align-items:flex-start;font-size:.85rem;color:#1f2937;cursor:pointer">
|
||||||
|
<input type="checkbox" id="ink-consent-chk" style="margin-top:2px;flex-shrink:0;width:16px;height:16px;accent-color:#1e3a5f">
|
||||||
|
<span>I authorize this and confirm the signature I draw is my own.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sig-block">
|
||||||
<div class="sig-tabs">
|
<div class="sig-tabs">
|
||||||
<button type="button" class="sig-tab active" data-mode="draw">Draw</button>
|
<button type="button" class="sig-tab active" data-mode="draw">Draw</button>
|
||||||
<button type="button" class="sig-tab" data-mode="type">Type</button>
|
<button type="button" class="sig-tab" data-mode="type">Type</button>
|
||||||
|
|
@ -95,6 +110,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
<input type="text" id="typed-sig" placeholder="Your full name" autocomplete="name">
|
<input type="text" id="typed-sig" placeholder="Your full name" autocomplete="name">
|
||||||
<div class="typed-preview" id="typed-preview"></div>
|
<div class="typed-preview" id="typed-preview"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3: Confirm -->
|
<!-- Step 3: Confirm -->
|
||||||
|
|
@ -190,6 +206,16 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
document.getElementById("perjury-box").classList.remove("hidden");
|
document.getElementById("perjury-box").classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ink-reproduction consent gate: for documents whose signature will be
|
||||||
|
// reproduced in ink on the official paper form, require an explicit
|
||||||
|
// per-document authorization BEFORE the signer can draw. Lock the signature
|
||||||
|
// block until the consent box is checked.
|
||||||
|
if (data.ink_reproduction && data.ink_consent_text) {
|
||||||
|
document.getElementById("ink-consent-text").textContent = data.ink_consent_text;
|
||||||
|
document.getElementById("ink-consent-box").classList.remove("hidden");
|
||||||
|
document.getElementById("sig-block").classList.add("locked");
|
||||||
|
}
|
||||||
|
|
||||||
// PDF preview
|
// PDF preview
|
||||||
if (data.document_url) {
|
if (data.document_url) {
|
||||||
document.getElementById("pdf-container").innerHTML =
|
document.getElementById("pdf-container").innerHTML =
|
||||||
|
|
@ -318,9 +344,31 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
var agreeChk = document.getElementById("agree-chk");
|
var agreeChk = document.getElementById("agree-chk");
|
||||||
agreeChk.addEventListener("change", updateSubmit);
|
agreeChk.addEventListener("change", updateSubmit);
|
||||||
|
|
||||||
|
// ── Ink-reproduction consent: unlock the signature block when authorized ──
|
||||||
|
var inkConsentChk = document.getElementById("ink-consent-chk");
|
||||||
|
inkConsentChk.addEventListener("change", function() {
|
||||||
|
var data = window._esignData || {};
|
||||||
|
var needsInk = !!(data.ink_reproduction && data.ink_consent_text);
|
||||||
|
if (needsInk) {
|
||||||
|
document.getElementById("sig-block").classList.toggle("locked", !this.checked);
|
||||||
|
// If they un-check after drawing, clear the now-unauthorized capture.
|
||||||
|
if (!this.checked) document.getElementById("sig-clear").click();
|
||||||
|
}
|
||||||
|
updateSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
|
function inkConsentOk() {
|
||||||
|
var data = window._esignData || {};
|
||||||
|
var needsInk = !!(data.ink_reproduction && data.ink_consent_text);
|
||||||
|
// Ink consent is only required for a DRAWN signature on an ink-path doc.
|
||||||
|
if (!needsInk || sigMode !== "draw") return true;
|
||||||
|
return inkConsentChk.checked;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSubmit() {
|
function updateSubmit() {
|
||||||
var hasSignature = sigMode === "draw" ? hasSig : typedInput.value.trim().length >= 2;
|
var hasSignature = sigMode === "draw" ? hasSig : typedInput.value.trim().length >= 2;
|
||||||
document.getElementById("submit-btn").disabled = !(hasSignature && agreeChk.checked);
|
document.getElementById("submit-btn").disabled =
|
||||||
|
!(hasSignature && agreeChk.checked && inkConsentOk());
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("submit-btn").addEventListener("click", async function() {
|
document.getElementById("submit-btn").addEventListener("click", async function() {
|
||||||
|
|
@ -353,6 +401,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
signature: signatureData,
|
signature: signatureData,
|
||||||
agreed_at: new Date().toISOString(),
|
agreed_at: new Date().toISOString(),
|
||||||
user_agent: navigator.userAgent,
|
user_agent: navigator.userAgent,
|
||||||
|
ink_consent: inkConsentOk() && sigMode === "draw" && inkConsentChk.checked,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
var result = await resp.json();
|
var result = await resp.json();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue