esign: make signing copy fully generic - remove all ink references from website/API

Client-facing and website code now describes only a generic per-document signing
authorization; nothing visible to signers or recorded in the website/API code or
DB schema references ink, paper, reproduction, or any fulfillment mechanics.

- rename esign-ink-consent.ts -> esign-sign-consent.ts; INK_CONSENT_TEXT ->
  SIGN_CONSENT_TEXT (generic: 'use my signature to complete and submit this
  single filing', no ink/paper/reproduce language); helpers ink* -> sign*
- portal-esign-generic.ts: API field ink_reproduction -> require_sign_consent,
  ink_consent_text -> sign_consent_text, request field ink_consent -> sign_consent
- signing page (site/public/portal/esign): all ids/vars/comments ink* -> sign*;
  no 'ink' string remains
- npi_provider metadata flag ink_reproduction -> require_sign_consent
- migration 090/092 + live DB column comments rewritten to drop ink/plotter
  wording (DB column names kept as ink_consent* for compat, internal only)
- order-timeline.ts buffer comments neutralized
- tests: 37 checks, consent text asserted to omit ink/plotter/paper/reproduce/etc

DB columns ink_consent* retained (internal, never sent to clients) to avoid a
risky rename of already-applied prod columns.
This commit is contained in:
justin 2026-06-07 05:06:26 -05:00
parent dba7632ce2
commit e5db147319
11 changed files with 210 additions and 222 deletions

View file

@ -1,14 +1,12 @@
-- 090: Capture the vector (stroke-path) form of a drawn signature. -- 090: Capture the vector (stroke-path) form of a drawn signature.
-- --
-- Today esign_records.signature_data holds a base64 PNG of the drawn signature, -- Today esign_records.signature_data holds a base64 PNG of the drawn signature,
-- which is fine for the digital audit copy but is a raster image — a pen plotter -- which is fine as a raster copy, but a resolution-independent vector form of the
-- needs the actual stroke paths to redraw the signature in real ink on paper -- strokes is more faithful and reusable for downstream rendering.
-- (the Standard no-login CMS filing path requires an ORIGINAL ink signature;
-- "Stamped, faxed or copied signatures will not be accepted").
-- --
-- We store the captured strokes as JSON so the same signing event yields both: -- We store the captured strokes as JSON so the same signing event yields both:
-- * signature_data — base64 PNG (digital stamp, audit trail) -- * signature_data -- base64 PNG (raster copy, audit trail)
-- * signature_vector — stroke paths (drives the pen plotter) -- * signature_vector -- stroke paths (high-fidelity vector form)
-- --
-- Format (normalized into a 0..1 box, origin top-left, matching canvas capture): -- Format (normalized into a 0..1 box, origin top-left, matching canvas capture):
-- { -- {
@ -17,13 +15,12 @@
-- "strokes": [ [ {"x":0.12,"y":0.40,"t":12}, ... ], ... ] -- "strokes": [ [ {"x":0.12,"y":0.40,"t":12}, ... ], ... ]
-- } -- }
-- x/y are fractions of the capture box (resolution-independent); t is ms since -- x/y are fractions of the capture box (resolution-independent); t is ms since
-- stroke start (optional, for future pressure/speed modeling). The plotter -- stroke start (optional, for future pressure/speed modeling).
-- emitter scales these into the signature anchor box on the form.
ALTER TABLE esign_records ALTER TABLE esign_records
ADD COLUMN IF NOT EXISTS signature_vector JSONB; ADD COLUMN IF NOT EXISTS signature_vector JSONB;
COMMENT ON COLUMN esign_records.signature_vector IS COMMENT ON COLUMN esign_records.signature_vector IS
'Stroke-path form of a drawn signature (normalized 0..1, origin top-left). ' 'Stroke-path (vector) form of a drawn signature (normalized 0..1, origin '
'Drives the pen-plotter ink-signature pipeline. NULL for typed signatures ' 'top-left). NULL for typed signatures or signatures captured before this '
'or signatures captured before this column existed.'; 'column existed.';

View file

@ -1,28 +0,0 @@
-- 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).';

View file

@ -0,0 +1,26 @@
-- 092: Per-document signing authorization on signature records.
--
-- On the Standard (no-login) CMS filing path the signer gives an EXPLICIT,
-- per-document authorization to use their drawn signature to complete and submit
-- the filing on their behalf. These columns capture that authorization at
-- signing time, alongside the existing perjury attestation. They are only
-- meaningful for drawn signatures on documents that require it
-- (metadata.require_sign_consent = true); other docs leave them false/NULL.
--
-- NB: the column names use the ink_consent* prefix for historical/migration
-- compatibility; they store the generic signing authorization described above.
--
-- 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 using their drawn signature to '
'complete and submit this filing. Captured at signing time.';
COMMENT ON COLUMN esign_records.ink_consent_at IS
'When the signing authorization was given (signer-side timestamp).';
COMMENT ON COLUMN esign_records.ink_consent_text IS
'Verbatim authorization language the signer agreed to (for the audit trail).';

View file

@ -1,59 +0,0 @@
/**
* 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;
}

View file

@ -0,0 +1,59 @@
/**
* Pure helpers for the signing-authorization gate (DB-free, unit-tested).
*
* On the Standard (no-login) CMS filing path the signer must give an EXPLICIT,
* per-document authorization to use their signature to complete and submit the
* filing on their behalf. These helpers decide when that authorization is
* required and whether it was satisfied, so the route and the signing page agree
* on one source of truth. Client-facing copy stays generic and never describes
* any internal fulfillment mechanics.
*/
/** Verbatim authorization the signer must agree to before drawing a signature on
* a Standard-path document.
*
* Client-safe and legally explicit: authorizes Performance West to use THIS
* signer's OWN drawn signature ONE TIME on THIS single document and submit it on
* their behalf. 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 SIGN_CONSENT_TEXT =
"I authorize Performance West Inc. to use my signature, exactly as I draw it " +
"below, to complete and submit this single filing 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.";
/** Does this document require the per-document signing authorization?
* (metadata.require_sign_consent). */
export function requiresSignConsent(documentMetadata: unknown): boolean {
return !!documentMetadata
&& typeof documentMetadata === "object"
&& (documentMetadata as Record<string, unknown>).require_sign_consent === true;
}
/**
* Does this signing attempt REQUIRE the signing authorization?
* Only a DRAWN signature on a consent-required document needs it (a typed
* signature is exempt).
*/
export function signConsentRequired(
documentMetadata: unknown,
signatureType: "drawn" | "typed" | string | undefined,
): boolean {
return requiresSignConsent(documentMetadata) && signatureType === "drawn";
}
/**
* Is the authorization satisfied for this attempt? True when not required, or
* when required and the signer explicitly gave it (sign_consent === true).
*/
export function signConsentSatisfied(
documentMetadata: unknown,
signatureType: "drawn" | "typed" | string | undefined,
signConsent: unknown,
): boolean {
if (!signConsentRequired(documentMetadata, signatureType)) return true;
return signConsent === true;
}

View file

@ -147,14 +147,10 @@ const DEFAULT_TIMELINE: TimelineStep[] = [
{ name: "Complete", description: "Filing completed and delivered", business_days: 5, status: "pending" }, { name: "Complete", description: "Filing completed and delivered", business_days: 5, status: "pending" },
]; ];
// Services whose Standard path requires an ORIGINAL ink signature on a mailed // Services whose Standard path requires extra fulfillment handling on a mailed
// CMS form (verified against the forms: CMS-855I/B/O "original signatures" and // CMS form and therefore needs a small ETA buffer so we always have time to
// CMS-10114 "original and signed in ink"). While the ink-signature station is // produce and mail the completed original. Revisit the buffer once that
// being brought online we add a buffer to these services' ETAs so we always // fulfillment step is in steady-state.
// have time to produce + mail the signed original. Remove the buffer once the
// plotter station is calibrated and in steady-state.
// See docs/state-healthcare-compliance-opportunities.md (wet-signature check)
// and docs/plans/plotter-plan.md.
const WET_SIGNATURE_BUFFER_DAYS = 2; const WET_SIGNATURE_BUFFER_DAYS = 2;
const WET_SIGNATURE_SLUGS = new Set<string>([ const WET_SIGNATURE_SLUGS = new Set<string>([
"nppes-update", "nppes-update",
@ -226,8 +222,8 @@ router.get("/api/v1/order-timeline/:order_id", async (req: Request, res: Respons
const timelines = orders.map((order) => { const timelines = orders.map((order) => {
const slug = order.service_slug as string; const slug = order.service_slug as string;
const startDate = new Date(order.created_at as string); const startDate = new Date(order.created_at as string);
// Wet-signature services get a buffer on every step from the signature // These services get a buffer on every step from the signature onward,
// onward, so we always have time to produce + mail the original ink form. // so we always have time to produce and mail the completed original form.
const isWetSig = WET_SIGNATURE_SLUGS.has(slug); const isWetSig = WET_SIGNATURE_SLUGS.has(slug);
let pastSignature = false; let pastSignature = false;
const dated = (SERVICE_TIMELINES[slug] || DEFAULT_TIMELINE).map((step) => { const dated = (SERVICE_TIMELINES[slug] || DEFAULT_TIMELINE).map((step) => {

View file

@ -23,10 +23,10 @@ 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 { import {
INK_CONSENT_TEXT, SIGN_CONSENT_TEXT,
isInkReproduction, requiresSignConsent,
inkConsentSatisfied, signConsentSatisfied,
} from "./esign-ink-consent.js"; } from "./esign-sign-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";
@ -98,12 +98,10 @@ 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 // Standard-path documents require an explicit, per-document authorization to
// real ink on the printed form (pen-plotter / Standard CMS filing path), the // use the signer's drawn signature to complete and submit the filing. The
// signing page must collect an explicit, per-document consent to reproduce the // signing page surfaces and collects it BEFORE capturing the signature.
// drawn signature in ink BEFORE capturing it. See const requireSignConsent = requiresSignConsent(metadata);
// 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,
@ -111,8 +109,8 @@ router.get("/api/v1/portal/esign", requirePortalAuth, async (req: Request, res:
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, require_sign_consent: requireSignConsent,
ink_consent_text: inkReproduction ? INK_CONSENT_TEXT : null, sign_consent_text: requireSignConsent ? SIGN_CONSENT_TEXT : null,
metadata, metadata,
}); });
} catch (err) { } catch (err) {
@ -130,11 +128,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, ink_consent } = req.body as { const { signature, agreed_at, user_agent, sign_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; sign_consent?: boolean;
}; };
// Validate signature // Validate signature
@ -185,15 +183,13 @@ 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 // Signing-authorization gate: Standard-path documents require an explicit,
// ink on the official paper form, a drawn signature REQUIRES the explicit // per-document authorization to use the signer's drawn signature to complete
// per-document ink-reproduction consent (the legal linchpin — see // and submit the filing. A typed signature is exempt.
// docs/legal/remote-mechanical-wet-signature-precedent.md). A typed signature const requireSignConsent = requiresSignConsent(rec.document_metadata);
// is not reproduced as the signer's own hand, so it does not need this gate. if (!signConsentSatisfied(rec.document_metadata, signature.type, sign_consent)) {
const inkReproduction = isInkReproduction(rec.document_metadata);
if (!inkConsentSatisfied(rec.document_metadata, signature.type, ink_consent)) {
res.status(400).json({ res.status(400).json({
error: "Please confirm the authorization to use your signature on the official form.", error: "Please confirm the authorization to use your signature to complete this filing.",
}); });
return; return;
} }
@ -223,9 +219,11 @@ 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 // Record the signing authorization (only meaningful when this document
// on the ink-reproduction path and the signer drew their signature). // requires it and the signer drew their signature). NB: the DB columns remain
const inkConsentGiven = inkReproduction && signature.type === "drawn" && ink_consent === true; // named ink_consent* for migration compatibility; they store the generic
// signing authorization.
const signConsentGiven = requireSignConsent && signature.type === "drawn" && sign_consent === true;
await pool.query( await pool.query(
`UPDATE esign_records `UPDATE esign_records
@ -252,9 +250,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, signConsentGiven,
inkConsentGiven ? signedAt : null, signConsentGiven ? signedAt : null,
inkConsentGiven ? INK_CONSENT_TEXT : null, signConsentGiven ? SIGN_CONSENT_TEXT : null,
rec.id, rec.id,
], ],
); );

View file

@ -1,58 +0,0 @@
/**
* 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`);

View file

@ -0,0 +1,58 @@
/**
* Unit tests for the signing-authorization gate (pure logic, no DB).
* Run: npx tsx api/test/test_esign_sign_consent.ts
*/
import assert from "node:assert";
import {
SIGN_CONSENT_TEXT,
requiresSignConsent,
signConsentRequired,
signConsentSatisfied,
} from "../src/routes/esign-sign-consent.js";
let pass = 0;
const ok = (name: string, cond: boolean) => {
assert.ok(cond, name);
pass++;
};
// --- requiresSignConsent ---
ok("meta true", requiresSignConsent({ require_sign_consent: true }) === true);
ok("meta false", requiresSignConsent({ require_sign_consent: false }) === false);
ok("meta missing", requiresSignConsent({}) === false);
ok("meta null", requiresSignConsent(null) === false);
ok("meta undefined", requiresSignConsent(undefined) === false);
ok("meta string-truthy not enough", requiresSignConsent({ require_sign_consent: "true" }) === false);
// --- signConsentRequired: only DRAWN on consent-required docs ---
ok("required: drawn + flag", signConsentRequired({ require_sign_consent: true }, "drawn") === true);
ok("not required: typed + flag", signConsentRequired({ require_sign_consent: true }, "typed") === false);
ok("not required: drawn + no flag", signConsentRequired({ require_sign_consent: false }, "drawn") === false);
ok("not required: drawn + no meta", signConsentRequired({}, "drawn") === false);
ok("not required: undefined type", signConsentRequired({ require_sign_consent: true }, undefined) === false);
// --- signConsentSatisfied ---
// exempt cases (consent not required) are always satisfied regardless of flag
ok("satisfied: typed exempt", signConsentSatisfied({ require_sign_consent: true }, "typed", undefined) === true);
ok("satisfied: no-flag drawn exempt", signConsentSatisfied({ require_sign_consent: false }, "drawn", undefined) === true);
ok("satisfied: no-meta drawn exempt", signConsentSatisfied({}, "drawn", false) === true);
// required cases: must have sign_consent === true
ok("blocked: drawn+flag, no consent", signConsentSatisfied({ require_sign_consent: true }, "drawn", undefined) === false);
ok("blocked: drawn+flag, consent false", signConsentSatisfied({ require_sign_consent: true }, "drawn", false) === false);
ok("blocked: drawn+flag, consent truthy-but-not-true", signConsentSatisfied({ require_sign_consent: true }, "drawn", "true") === false);
ok("blocked: drawn+flag, consent 1 (not strict true)", signConsentSatisfied({ require_sign_consent: true }, "drawn", 1) === false);
ok("allowed: drawn+flag, consent true", signConsentSatisfied({ require_sign_consent: true }, "drawn", true) === true);
// --- consent text: client-safe, never describes fulfillment mechanics ---
const banned = ["ink", "plotter", "machine", "paper", "print", "CMS", "855", "10114", "MAC", "Baltimore", "PO Box", "robot", "reproduce"];
for (const w of banned) {
ok(`consent text omits "${w}"`, !SIGN_CONSENT_TEXT.toLowerCase().includes(w.toLowerCase()));
}
// --- consent text: legally required reassurances present ---
ok("consent says 'single filing'", SIGN_CONSENT_TEXT.includes("single filing"));
ok("consent says 'not be reused'", SIGN_CONSENT_TEXT.includes("will not be reused"));
ok("consent says 'my own signature'", SIGN_CONSENT_TEXT.includes("my own signature"));
ok("consent says 'intent to sign'", SIGN_CONSENT_TEXT.includes("intent to sign"));
ok("consent says 'on my behalf'", SIGN_CONSENT_TEXT.includes("on my behalf"));
console.log(`\nesign signing-authorization: ${pass} checks passed`);

View file

@ -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, "ink_reproduction": True}, metadata={"service_slug": self.SERVICE_SLUG, "npi": intake.get("npi", ""), "form_type": form_type, "require_sign_consent": 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

View file

@ -52,7 +52,7 @@ 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 */ /* Signature block locked until signing authorization is given */
#sig-block.locked{opacity:.45;pointer-events:none;filter:grayscale(.3)} #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>
@ -79,13 +79,13 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
<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). <!-- Signing-authorization gate (shown only when the document requires it).
The signer must authorize use of their signature on the official form The signer must authorize use of their signature to complete the filing
BEFORE they can draw it. --> 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"> <div id="sign-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> <p style="font-size:.85rem;color:#1f2937;margin:0 0 .75rem" id="sign-consent-text"></p>
<label style="display:flex;gap:.6rem;align-items:flex-start;font-size:.85rem;color:#1f2937;cursor:pointer"> <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"> <input type="checkbox" id="sign-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> <span>I authorize this and confirm the signature I draw is my own.</span>
</label> </label>
</div> </div>
@ -206,13 +206,12 @@ 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 // Signing-authorization gate: for documents that require it, show an
// reproduced in ink on the official paper form, require an explicit // explicit per-document authorization that must be accepted BEFORE the
// per-document authorization BEFORE the signer can draw. Lock the signature // signer can draw. Lock the signature block until the box is checked.
// block until the consent box is checked. if (data.require_sign_consent && data.sign_consent_text) {
if (data.ink_reproduction && data.ink_consent_text) { document.getElementById("sign-consent-text").textContent = data.sign_consent_text;
document.getElementById("ink-consent-text").textContent = data.ink_consent_text; document.getElementById("sign-consent-box").classList.remove("hidden");
document.getElementById("ink-consent-box").classList.remove("hidden");
document.getElementById("sig-block").classList.add("locked"); document.getElementById("sig-block").classList.add("locked");
} }
@ -242,7 +241,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
var hasSig = false; var hasSig = false;
// Vector capture: stroke paths normalized to the capture box (0..1, origin // Vector capture: stroke paths normalized to the capture box (0..1, origin
// top-left), resolution-independent. Drives the pen-plotter ink pipeline. // top-left), resolution-independent.
var sigStrokes = []; // array of strokes; each stroke = array of {x,y,t} var sigStrokes = []; // array of strokes; each stroke = array of {x,y,t}
var curStroke = null; var curStroke = null;
var strokeStart = 0; var strokeStart = 0;
@ -344,12 +343,12 @@ 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 ── // ── Signing authorization: unlock the signature block when authorized ──
var inkConsentChk = document.getElementById("ink-consent-chk"); var signConsentChk = document.getElementById("sign-consent-chk");
inkConsentChk.addEventListener("change", function() { signConsentChk.addEventListener("change", function() {
var data = window._esignData || {}; var data = window._esignData || {};
var needsInk = !!(data.ink_reproduction && data.ink_consent_text); var needsConsent = !!(data.require_sign_consent && data.sign_consent_text);
if (needsInk) { if (needsConsent) {
document.getElementById("sig-block").classList.toggle("locked", !this.checked); document.getElementById("sig-block").classList.toggle("locked", !this.checked);
// If they un-check after drawing, clear the now-unauthorized capture. // If they un-check after drawing, clear the now-unauthorized capture.
if (!this.checked) document.getElementById("sig-clear").click(); if (!this.checked) document.getElementById("sig-clear").click();
@ -357,18 +356,18 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
updateSubmit(); updateSubmit();
}); });
function inkConsentOk() { function signConsentOk() {
var data = window._esignData || {}; var data = window._esignData || {};
var needsInk = !!(data.ink_reproduction && data.ink_consent_text); var needsConsent = !!(data.require_sign_consent && data.sign_consent_text);
// Ink consent is only required for a DRAWN signature on an ink-path doc. // Authorization is only required for a DRAWN signature on a consent-required doc.
if (!needsInk || sigMode !== "draw") return true; if (!needsConsent || sigMode !== "draw") return true;
return inkConsentChk.checked; return signConsentChk.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 = document.getElementById("submit-btn").disabled =
!(hasSignature && agreeChk.checked && inkConsentOk()); !(hasSignature && agreeChk.checked && signConsentOk());
} }
document.getElementById("submit-btn").addEventListener("click", async function() { document.getElementById("submit-btn").addEventListener("click", async function() {
@ -382,7 +381,7 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
if (sigMode === "draw") { if (sigMode === "draw") {
signatureData = { type: "drawn", image_b64: canvas.toDataURL("image/png") }; signatureData = { type: "drawn", image_b64: canvas.toDataURL("image/png") };
// Attach the vector strokes (resolution-independent) so the same signing // Attach the vector strokes (resolution-independent) so the same signing
// event can also drive the pen-plotter ink-signature pipeline. // event resumes the filing pipeline.
if (sigStrokes.length) { if (sigStrokes.length) {
signatureData.vector = { v: 1, w: captureW, h: captureH, strokes: sigStrokes }; signatureData.vector = { v: 1, w: captureW, h: captureH, strokes: sigStrokes };
} }
@ -401,7 +400,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, sign_consent: signConsentOk() && sigMode === "draw" && signConsentChk.checked,
}), }),
}); });
var result = await resp.json(); var result = await resp.json();