npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@e-sig/core

v0.6.0

Published

Self-contained PKCS#7 PDF signing — no SaaS, no metering, no per-doc fees. Render→sign→verify engine + pluggable persistence interfaces + end-to-end signDocument() orchestrator.

Readme

@e-sig/core — Portable In-Platform E-Signature

Self-contained PKCS#7 PDF signing — no SaaS, no metering, no per-doc fees. Battle-tested in production at opendelphi.org.

This directory is the portable core of the Opendelphi e-signature pipeline. Drop it (plus a tiny adapter you write) into any TypeScript / Node.js project to add real cryptographic signing of PDFs.


What it does

Given an HTML document and a person who wants to sign it, this library:

  1. Renders the HTML to a PDF (headless Chromium via puppeteer-core; scripting disabled by default).
  2. Generates or reuses a self-signed RSA-2048 X.509 cert for the signing tenant.
  3. Embeds a PKCS#7 detached signature under the ETSI.CAdES.detached subfilter, with the ESS signing-certificate-v2 attribute binding the signer cert into the signed data. Pass padesStrict: true for strict PAdES B-B (also drops the PAdES-forbidden signing-time attribute).
  4. Produces a PDF that opens cleanly in Preview / Adobe Reader with a valid signature panel — any post-signing edit invalidates the signature. verifyPdfSignature() checks this cryptographically (recomputes the digest over the signed ByteRange and RSA-verifies the signature).
  5. (Optional) Adds a post-quantum hybrid sealEd25519 + ML-DSA-65 (FIPS 204) — embedded under the classical signature so the PDF stays Acrobat-valid while gaining quantum resistance. See Post-quantum seal.

Trust vs. validity. The signature is cryptographically valid, but the cert is self-issued — stock Adobe Reader shows "validity unknown" until the cert is trusted (org trust-store import, or plug in an AATL/CA signer). This verifies the signature math and integrity, not third-party trust. See the compliance notes below.

That's the whole thing. It's ~600 lines of TypeScript with zero runtime dependencies on Supabase, Next.js, or any SaaS.


What it does NOT do

By design — these are wrapper concerns:

  • Storage of the signed PDF. You decide where (S3, local disk, Supabase Storage, …).
  • Auth / authorization. You decide who's allowed to sign.
  • UI. You build the signature-capture surface.
  • Persistence of the cert pool. Implement the CertStore adapter interface.
  • Audit logging. Implement the AuditLogStore adapter interface.

The library gives you crypto + rendering. You bring persistence + UI + auth.


Files

| File | Purpose | Project-agnostic? | |---|---|---| | pem-signer.ts | Custom @signpdf Signer driven by raw PEM key+cert (bypasses node-forge's broken P12 round-trip — see Background below) | ✅ | | cert-issuer.ts | Generate self-signed RSA-2048 X.509; AES-256-GCM-wrap private keys for at-rest storage | ✅ | | render-pdf.ts | HTML → PDF via puppeteer-core; auto-detects Lambda vs local Chrome | ✅ | | sign-pdf.ts | Combine placeholder injection + PKCS#7 sign (ETSI.CAdES) | ✅ | | verify-pdf.ts | Structural verifier (parses /ByteRange + PKCS#7 blob, returns diagnostics) | ✅ | | pq-seal.ts | Hybrid Ed25519 + ML-DSA-65 (FIPS 204) post-quantum seal — keygen, sign, verify | ✅ | | pq-embed.ts | Embed/extract the seal in a PDF as an append-only incremental update | ✅ | | pq-verify.ts | verifyPqSeal + verifyDocument (classical and post-quantum verdict) | ✅ | | pq-lifecycle.ts | ensureActivePqKeys / rotatePqKeys over a bring-your-own PqKeyStore | ✅ | | signature-block.ts | HTML helper to render N signature blocks for multi-party flows | ✅ | | types.ts | Shared TS types (Signer, SigningCertPem, …) | ✅ | | index.ts | Public re-export barrel | ✅ |


Install

npm install \
  @signpdf/signpdf \
  @signpdf/utils \
  @signpdf/placeholder-plain \
  node-forge \
  puppeteer-core \
  @sparticuz/chromium  # only on Lambda; skip for local-only

Plus @types/node-forge if you're using TypeScript.

If you're on Next.js, you MUST externalize the binary-adjacent packages in next.config.ts:

const nextConfig = {
  serverExternalPackages: [
    "@sparticuz/chromium",
    "puppeteer-core",
    "node-forge",
    "@signpdf/signpdf",
    "@signpdf/utils",
    "@signpdf/placeholder-plain",
  ],
  // The chromium binary tarball is a static asset; tell file-tracing to
  // include it in your e-sig route's bundle.
  outputFileTracingIncludes: {
    "/api/your-esig-route": [
      "./node_modules/@sparticuz/chromium/bin/**",
    ],
  },
};

30-second example

import {
  generateSelfSignedCert,
  renderHtmlToPdf,
  signPdf,
  verifyPdfStructure,
} from "@e-sig/core";

// 1. Issue a one-off cert (in real life, persist + reuse).
const cert = generateSelfSignedCert({ subjectName: "Acme Corp" });

// 2. Render HTML → unsigned PDF.
const unsigned = await renderHtmlToPdf({
  html: `<h1>Service Agreement</h1><p>Signed by Jane Doe at ${new Date().toISOString()}.</p>`,
});

// 3. Sign it.
const { signedPdf } = await signPdf({
  pdf: unsigned,
  keyPem: cert.keyPem,
  certPem: cert.certPem,
  reason: "Service Agreement acceptance",
  location: "https://acme.example",
  contactInfo: "[email protected]",
  name: "Jane Doe",
});

// 4. Verify the result cryptographically (also exported as verifyPdfSignature).
//    ok === true only when structure + document digest + RSA signature all pass;
//    a single flipped byte under the signature makes ok=false / digestValid=false.
const verify = verifyPdfStructure(signedPdf);
console.log(verify.ok, verify.digestValid, verify.signatureValid, verify.signerCommonName);
// → true, true, true, "E-sig (Acme Corp)"

// 5. Persist + serve. Up to you.
require("fs").writeFileSync("./signed.pdf", signedPdf);

That's it. Open signed.pdf in Preview — signature panel shows valid (self-signed).


RFC 3161 trusted timestamps (CAdES-T)

Pass a tsa transport to signPdf to embed an RFC 3161 TimeStampToken, upgrading the signature from CAdES-B to CAdES-T. The token is added as the id-aa-timeStampToken unsigned attribute (OID 1.2.840.113549.1.9.16.2.14) computed over the SignerInfo signatureValue (RFC 3161 §2.4.1).

The package performs no network egress — you inject the POST so the package stays dependency-free. The TSA only ever receives a SHA-256 hash, never the document or any PHI:

import type { TsaTransport } from "@e-sig/core";

const tsa: TsaTransport = {
  required: false, // false = degrade to CAdES-B on TSA failure; true = throw
  fetch: async (reqDerBytes) => {
    const res = await fetch("http://timestamp.digicert.com", {
      method: "POST",
      headers: { "Content-Type": "application/timestamp-query" },
      body: reqDerBytes,
    });
    return new Uint8Array(await res.arrayBuffer());
  },
};

const { signedPdf, timestamped, tsaError } = await signPdf({
  pdf, keyPem, certPem,
  reason: "DUA acceptance", location: "opendelphi.org",
  contactInfo: "[email protected]", name: "Acme Research Institute",
  tsa,
});

const v = verifyPdfStructure(signedPdf);
// v.timestamped, v.timestampTime (ISO), v.tsaCommonName
// v.ok is false if the §2.4.2 binding check fails (imprint != sha256(sigValue))

Notes:

  • Budget: when tsa is supplied and signatureLength is omitted, the /Contents placeholder budget defaults to 30720 (vs 8192 without a TSA) to fit the TimeStampToken plus the TSA certificate chain. An overflow is rejected, never silently truncated.
  • Degradation: with required: false (default), a TSA error produces a valid CAdES-B signature and sets tsaError; with required: true the error is rethrown.
  • Verification enforces the RFC 3161 §2.4.2 binding: the token's messageImprint must equal sha256(SignerInfo.signature), else ok:false.

See CONSUMING.md for the full consumer guide.


Post-quantum seal — ML-DSA-65 (FIPS 204)

Harvest-now-decrypt-later is a real threat to long-lived signed documents: a signature that is only RSA/ECDSA today is forgeable the day a cryptographically relevant quantum computer exists. @e-sig/core can add a hybrid post-quantum sealEd25519 + ML-DSA-65 (the FIPS 204 module-lattice signature, née Dilithium) — to any signed PDF, without giving up compatibility.

How it stays compatible: the seal does not replace the PKCS#7/PAdES RSA signature (no mainstream PDF reader validates ML-DSA in PAdES yet). The seal is embedded first, as an append-only incremental update, and the classical RSA signature is applied on top — so it cryptographically covers the seal. Adobe Acrobat still shows a valid signature; your verifier additionally confirms the quantum-resistant layer. This is the hybrid migration path NIST/CNSA 2.0 recommend over a hard cutover.

import {
  generateSelfSignedCert,
  generatePqKeyBundle,
  loadPqSigningKeys,
  signPdf,
  verifyDocument,
} from "@e-sig/core";

const cert = generateSelfSignedCert({ subjectName: "Acme Corp" });

// One hybrid key bundle per signer/tenant (persist wrapPqKeyBundle(...) at rest;
// or use ensureActivePqKeys with a PqKeyStore — see below).
const pqKeys = loadPqSigningKeys(generatePqKeyBundle().bundle);

const { signedPdf } = await signPdf({
  pdf: unsigned,
  keyPem: cert.keyPem,
  certPem: cert.certPem,
  reason: "Service Agreement acceptance",
  location: "",
  contactInfo: "[email protected]",
  name: "Jane Doe",
  pqSeal: { keys: pqKeys },   // ← adds the post-quantum seal
});

// One call, two verdicts:
const v = verifyDocument(signedPdf);
console.log("classical (PAdES/RSA):", v.classical.ok);          // → true (Acrobat-grade)
console.log("post-quantum (ML-DSA-65):", v.postQuantum.ok);     // → true
console.log("PQ signer fingerprint:", v.postQuantum.mldsa65Fpr);
// v.ok === true only when BOTH layers verify AND the seal lies inside the
// RSA-signed region. Tampering with one byte of the document fails BOTH.

Managed keys. ensureActivePqKeys({ store, tenantId, passphrase }) mints + wraps a bundle on first use and reuses it thereafter (implement the small PqKeyStore interface against your DB, same pattern as CertStore); rotatePqKeys(...) rolls to a fresh key while old documents keep verifying against the public key embedded in each seal. signDocument({ pq: { keys } }) threads it through the end-to-end orchestrator and records the PQ key id + ML-DSA fingerprint in the audit row.

Trust model (v1). The seal carries the raw ML-DSA-65 public key + its SHA-256 fingerprint; a relying party pins/publishes the expected fingerprint (TOFU). Assert it in-band:

verifyDocument(signedPdf, {
  expectedMldsa65Fpr: "<the signer's published fingerprint>", // fails if it differs
  requirePq: true,                                            // fails if no seal present (no silent downgrade)
});

X.509 identity (RFC 9881). For an enterprise-shaped identity, issue a self-signed ML-DSA-65 X.509 certificate (OID 2.16.840.1.101.3.4.3.18) — its SubjectPublicKeyInfo and signature are ML-DSA-65, so it parses and verifies in OpenSSL 3.5+ (validated against 3.6). Bind it to a seal at verify time:

const cert = issueMlDsaCertificate({ keys, subjectName: "Acme Inc" }); // publish cert.certPem
verifyDocument(signedPdf, { signerCert: cert.certPem }); // fails unless the cert is valid AND owns the seal's key

verifyMlDsaCertificate() checks the self-signature, algorithm, and validity window; certMatchesPqSeal() ties it to a seal by public-key fingerprint. Both seal signatures are required — if either Ed25519 or ML-DSA-65 fails (or the seal's fingerprint/keyId don't match its keys), the seal fails.


Persisting certs + audit logs across requests

For real usage you need to:

  • Cache certs per tenant so you don't regenerate on every sign.
  • Encrypt private keys at rest so a DB leak doesn't compromise signing authority.
  • Log every sign for ESIGN / UETA / 21 CFR §11 compliance evidence.

The library provides adapter interfaces (CertStore, AuditLogStore — see ../adapters/types.ts). Implement them against your DB.

A reference Supabase implementation lives at ../adapters/supabase.ts (~150 lines). It works against any schema with these two tables — copy the migration from supabase/migrations/00106_esig_self_contained.sql for the canonical shape, or write your own.

CertStore interface

interface CertStore {
  findActive(tenantId: string): Promise<StoredCert | null>;
  insert(input: { tenantId; generated; keyPemEncrypted; rotatedFromId? }): Promise<StoredCert>;
  deactivate(id: string): Promise<void>;
  findExpiring(withinDays: number): Promise<StoredCert[]>;
}

AuditLogStore interface

interface AuditLogStore {
  insert(entry: AuditLogEntry): Promise<AuditLogRow>;
}

Then use the convenience helper ensureActiveCert from ../adapters/supabase.ts as a template:

const result = await ensureActiveCert({
  store: new YourCertStore(...),
  tenantId: "acme-corp",
  subjectName: "Acme Corp",
  passphrase: process.env.ESIG_CERT_PASSPHRASE!,
});
// result.certPem + result.keyPem ready to feed into signPdf()

Compliance posture

The Opendelphi production wire-up uses this library for HIPAA-bound Data Use Agreements and is mapped against:

  • ESIGN Act § 7001 (R1–R5) — Intent, Consent to electronic, Attribution, Integrity, Retention. R4 Integrity is fully covered by the crypto core; R1/R2/R3/R5 are wrapper concerns.
  • UETA § 9 + § 13 — Attribution + system attribution log.
  • 21 CFR § 11.50 / § 11.70 — FDA-grade requirements where applicable.

See .planning/phases/19-esig-primitives-spike/19-04-ESIGN-GAPS.md for the full mapping.

Not legal advice. Talk to your lawyer about whether this satisfies the regulatory framework for your specific use case.


Background — why this exists

Why not DocuSign / DocuSeal / Documenso?

Per-document metering (~$0.20/sig) made the unit economics painful at scale. And every signed PDF flowed through a third-party processor — making HIPAA + GDPR compliance harder than it had to be.

This library is what you reach for when "no SaaS, no metering, no fees" is a hard requirement.

Why not pdf-lib?

pdf-lib is the most popular Node PDF library, but it hasn't shipped a release since 2021. Documenso uses @libpdf/core instead — same conclusion here. (Neither is actually used by this core — we drive puppeteer for rendering and @signpdf + node-forge for signing, both actively maintained.)

Why not PKCS#12?

We tried. node-forge.pkcs12.toPkcs12Asn1 produces P12 bundles whose MAC neither node-forge nor openssl can verify. Looks like a long-standing BMPString-password-derivation bug. We bypass it entirely — the PemSigner takes raw PEM and drives forge.pkcs7 directly.

Bugs to avoid (we hit these so you don't have to)

  1. Don't use node-forge.pkcs12.toPkcs12Asn1 — see above.
  2. ASCII-only cert subject namesforge.pki.certificateFromPem mis-counts bytes for non-ASCII (em-dash in OU breaks PEM round-trip with "Too few bytes to parse DER").
  3. @signpdf/signpdf v3 ESM default import is opaque — use the named export: import { SignPdf } from "@signpdf/signpdf" and new SignPdf().sign(...).
  4. @sparticuz/chromium is Lambda-only — locally, use system Chrome via the executablePath override.
  5. Storing key + cert in one PEM file is fragileforge.pem.decode extracts blocks correctly, but re-encoding the cert block from a multi-block buffer doesn't survive certificateFromPem round-trip. Store them as separate files / DB columns.

Performance

End-to-end on Vercel Lambda (cold start), tested against opendelphi.org in production:

  • Render HTML → unsigned PDF: ~2.5 s (cold) / ~0.5 s (warm)
  • Generate cert (first sign per tenant): ~0.8 s (RSA-2048 keygen dominates)
  • PKCS#7 sign: ~0.1 s
  • Upload + audit + DB row flip: ~0.3 s
  • Total cold-start round-trip: ~4.5 s

Subsequent signs reuse the cached cert → ~1–1.5 s warm.


License

Same as the parent project. The core/ directory is intentionally self-contained so it can be vendored under your own license.


Acknowledgments

  • @signpdf for the PKCS#7 placeholder + signing infrastructure.
  • node-forge for the X.509 + PKCS#7 + crypto primitives.
  • Documenso + DocuSeal CE as reference implementations (read-only — no code copied — see .planning/phases/19-esig-primitives-spike/19-02-PATTERNS-OBSERVED.md for the lessons borrowed).