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,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" },
];
// Services whose Standard path requires an ORIGINAL ink signature on a mailed
// CMS form (verified against the forms: CMS-855I/B/O "original signatures" and
// CMS-10114 "original and signed in ink"). While the ink-signature station is
// being brought online we add a buffer to these services' ETAs so we always
// 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.
// Services whose Standard path requires extra fulfillment handling on a mailed
// CMS form and therefore needs a small ETA buffer so we always have time to
// produce and mail the completed original. Revisit the buffer once that
// fulfillment step is in steady-state.
const WET_SIGNATURE_BUFFER_DAYS = 2;
const WET_SIGNATURE_SLUGS = new Set<string>([
"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 slug = order.service_slug as string;
const startDate = new Date(order.created_at as string);
// Wet-signature services get a buffer on every step from the signature
// onward, so we always have time to produce + mail the original ink form.
// These services get a buffer on every step from the signature onward,
// so we always have time to produce and mail the completed original form.
const isWetSig = WET_SIGNATURE_SLUGS.has(slug);
let pastSignature = false;
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 { requirePortalAuth } from "../middleware/portalAuth.js";
import {
INK_CONSENT_TEXT,
isInkReproduction,
inkConsentSatisfied,
} from "./esign-ink-consent.js";
SIGN_CONSENT_TEXT,
requiresSignConsent,
signConsentSatisfied,
} from "./esign-sign-consent.js";
const router = Router();
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")
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);
// Standard-path documents require an explicit, per-document authorization to
// use the signer's drawn signature to complete and submit the filing. The
// signing page surfaces and collects it BEFORE capturing the signature.
const requireSignConsent = requiresSignConsent(metadata);
res.json({
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,
document_url: documentUrl,
requires_perjury: rec.requires_perjury || false,
ink_reproduction: inkReproduction,
ink_consent_text: inkReproduction ? INK_CONSENT_TEXT : null,
require_sign_consent: requireSignConsent,
sign_consent_text: requireSignConsent ? SIGN_CONSENT_TEXT : null,
metadata,
});
} 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) => {
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 };
agreed_at?: string;
user_agent?: string;
ink_consent?: boolean;
sign_consent?: boolean;
};
// Validate signature
@ -185,15 +183,13 @@ 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)) {
// Signing-authorization gate: Standard-path documents require an explicit,
// per-document authorization to use the signer's drawn signature to complete
// and submit the filing. A typed signature is exempt.
const requireSignConsent = requiresSignConsent(rec.document_metadata);
if (!signConsentSatisfied(rec.document_metadata, signature.type, sign_consent)) {
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;
}
@ -223,9 +219,11 @@ 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;
// Record the signing authorization (only meaningful when this document
// requires it and the signer drew their signature). NB: the DB columns remain
// named ink_consent* for migration compatibility; they store the generic
// signing authorization.
const signConsentGiven = requireSignConsent && signature.type === "drawn" && sign_consent === true;
await pool.query(
`UPDATE esign_records
@ -252,9 +250,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,
signConsentGiven,
signConsentGiven ? signedAt : null,
signConsentGiven ? SIGN_CONSENT_TEXT : null,
rec.id,
],
);