{"_id":"@clocklobster/signing-pdf","name":"@clocklobster/signing-pdf","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@clocklobster/signing-pdf","publishConfig":{"access":"public"},"version":"1.0.0","description":"Pure pdf-lib primitives for electronic-signature PDF overlay: field values, signature images, certificate pages, and finalization.","license":"MIT","author":{"name":"Victor Salmon"},"repository":{"type":"git","url":"git+https://github.com/victorsalmon/signing-pdf.git"},"homepage":"https://github.com/victorsalmon/signing-pdf#readme","bugs":{"url":"https://github.com/victorsalmon/signing-pdf/issues"},"keywords":["pdf","pdf-lib","electronic-signature","e-sign","esign","signature","certificate","overlay","document"],"type":"module","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc -p tsconfig.build.json","postinstall":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit","test":"vitest run"},"dependencies":{"pdf-lib":"^1.17.1"},"devDependencies":{"@types/node":"^22.10.2","typescript":"^5.7.2","vitest":"^3.2.7"},"engines":{"node":">=18.0.0"},"gitHead":"321c0682bf9477a10316e10818fdaeff817aaf1d","_id":"@clocklobster/signing-pdf@1.0.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-ohmGosjrrNl6iMfBGdNBX/m+BRqUP4KtQ0X3+dCoQigL9zad5PktU3DvrcfDdQbIxlsLSjj0OTyZUpjH2Rsxpw==","shasum":"c252d68208c2dfe06d417446efbbb39c4a7151dc","tarball":"https://registry.npmjs.org/@clocklobster/signing-pdf/-/signing-pdf-1.0.0.tgz","fileCount":13,"unpackedSize":29430,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCw/UWKxtylZWPv3d9yAgvugl0LJWi7VblJWMfszimFLQIhAKzipCB+d+LlGgBlvcjz0ZBd2/cl2uHinxJVT4Vqdx52"}]},"_npmUser":{"name":"vasalmon","email":"vasalmon@hotmail.com"},"directories":{},"maintainers":[{"name":"vasalmon","email":"vasalmon@hotmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/signing-pdf_1.0.0_1787354972033_0.22798179551085518"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-21T23:29:31.816Z","1.0.0":"2026-08-21T23:29:32.268Z","modified":"2026-08-21T23:29:32.696Z"},"maintainers":[{"name":"vasalmon","email":"vasalmon@hotmail.com"}],"description":"Pure pdf-lib primitives for electronic-signature PDF overlay: field values, signature images, certificate pages, and finalization.","homepage":"https://github.com/victorsalmon/signing-pdf#readme","keywords":["pdf","pdf-lib","electronic-signature","e-sign","esign","signature","certificate","overlay","document"],"repository":{"type":"git","url":"git+https://github.com/victorsalmon/signing-pdf.git"},"author":{"name":"Victor Salmon"},"bugs":{"url":"https://github.com/victorsalmon/signing-pdf/issues"},"license":"MIT","readme":"# signing-pdf\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)\n[![pdf-lib](https://img.shields.io/badge/pdf--lib-1.17-red.svg)](https://pdf-lib.js.org/)\n[![Tests](https://img.shields.io/badge/tests-8%20passing-brightgreen.svg)](#testing)\n\nPure [`pdf-lib`](https://pdf-lib.js.org/) primitives for electronic-signature PDF overlay —\nfield values, signature images, certificate pages, and finalization. **No AWS, no storage,\nno product coupling — just PDF manipulation.**\n\n> **Why this exists:** Building e-signature PDF handling from scratch with `pdf-lib` is\n> fiddly — coordinate systems (top-left vs bottom-left origin), WinAnsi encoding sanitization,\n> base64 PNG embedding, and certificate page layout all need careful handling. This package\n> wraps those primitives behind a clean, typed, tested surface so you can build a\n> self-hosted e-signature flow without reinventing the PDF layer.\n\n---\n\n## Table of contents\n\n- [Overview](#overview)\n- [Features](#features)\n- [Install](#install)\n- [Quick start](#quick-start)\n- [API reference](#api-reference)\n  - [`loadPdf(bytes)`](#loadpdfbytes)\n  - [`embedFieldValues(pdf, fields, values, options?)`](#embedfieldvaluespdf-fields-values-options)\n  - [`embedSignatureImage(pdf, imageBase64, overlay)`](#embedsignatureimagepdf-imagebase64-overlay)\n  - [`embedCertificatePage(pdf, data, options?)`](#embedcertificatepagepdf-data-options)\n  - [`finalizeSignedPdf(pdf)`](#finalizesignedpdfpdf)\n  - [`sanitizeWinAnsi(text)`](#sanitizewinanitext)\n  - [Re-exports](#re-exports)\n  - [Types](#types)\n- [Coordinate system](#coordinate-system)\n- [WinAnsi sanitization](#winansi-sanitization)\n- [Field types](#field-types)\n- [Certificate page](#certificate-page)\n- [Testing](#testing)\n- [Development](#development)\n- [Project layout](#project-layout)\n- [Usage in an e-signature flow](#usage-in-an-e-signature-flow)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Overview\n\n`signing-pdf` provides the PDF manipulation primitives needed to build an electronic-signature\nflow on top of `pdf-lib`:\n\n- **Load** a PDF from bytes\n- **Embed field values** — draw text (names, dates, addresses) at specified coordinates\n- **Embed signature images** — place base64-encoded PNG signature images on the document\n- **Embed a certificate page** — append a \"Certificate of Completion\" page with signer\n  details, timestamps, IP/User-Agent, and an integrity hash\n- **Finalize** — save the modified PDF back to bytes\n- **Sanitize WinAnsi** — strip/replace characters that `pdf-lib`'s standard fonts can't render\n\nThe package is intentionally low-level: it manipulates PDFs and nothing else. Storage\n(S3, local FS), email sending, envelope lifecycle, and tenant management live in the\nconsuming application.\n\n---\n\n## Features\n\n- **Field value overlay** — draw text at top-left-origin coordinates (the package handles\n  the conversion to `pdf-lib`'s bottom-left origin)\n- **Signature image embedding** — accepts base64 PNG (with or without the\n  `data:image/png;base64,` prefix); validates the PNG before embedding\n- **Certificate of Completion page** — appends a formatted page with document title,\n  envelope ID, completion timestamp, signer list (name, email, role, signed-at, IP,\n  User-Agent), and a SHA-256 integrity hash\n- **WinAnsi sanitization** — replaces smart quotes, em/en dashes, ellipses, and strips\n  control characters so `pdf-lib`'s standard fonts (Helvetica, etc.) don't throw on\n  Unicode input\n- **Role-based field types** — fields carry an optional `role` and `type` for per-signer\n  filtering and variable resolution in higher layers\n- **Color support** — per-field text color via `{ r, g, b }` (0–1 floats)\n- **Font injection** — pass custom fonts or use the defaults (Helvetica / HelveticaBold)\n- **pdf-lib re-exports** — `PDFDocument`, `PDFPage`, `PDFFont`, `StandardFonts`, `rgb`\n  are re-exported so consumers don't need a separate `pdf-lib` import\n- **Pure functions** — no side effects beyond the PDF document passed in\n\n---\n\n## Install\n\n```bash\nnpm install @clocklobster/signing-pdf\n# or\npnpm add @clocklobster/signing-pdf\n```\n\n### Dependencies\n\n- [`pdf-lib`](https://www.npmjs.com/package/pdf-lib) `^1.17.1` (runtime dependency)\n\n### Requirements\n\n- **Node.js >= 18** (uses `Buffer` for base64 decoding)\n- **TypeScript >= 5** (for type consumers; ships `.d.ts` files)\n\n---\n\n## Quick start\n\n```typescript\nimport {\n  loadPdf,\n  embedFieldValues,\n  embedSignatureImage,\n  embedCertificatePage,\n  finalizeSignedPdf,\n  sanitizeWinAnsi,\n  PDFDocument,\n  StandardFonts,\n} from '@clocklobster/signing-pdf';\n\n// 1. Load the template PDF\nconst pdfBytes = await fs.readFile('agreement-template.pdf');\nconst pdf = await loadPdf(new Uint8Array(pdfBytes));\n\n// 2. Embed field values (text fields)\nawait embedFieldValues(\n  pdf,\n  [\n    { key: 'clientName', page: 1, x: 100, y: 200, width: 250, fontSize: 11 },\n    { key: 'date', page: 1, x: 100, y: 220, width: 100, fontSize: 11 },\n    { key: 'address', page: 1, x: 100, y: 240, width: 300, fontSize: 10 },\n  ],\n  {\n    clientName: 'Jane Doe',\n    date: '2026-08-21',\n    address: '123 Main St, Toronto, ON',\n  }\n);\n\n// 3. Embed a signature image (base64 PNG from a signature pad)\nconst signatureBase64 = 'data:image/png;base64,iVBORw0KGgo...';\nawait embedSignatureImage(pdf, signatureBase64, {\n  page: 1,\n  x: 100,\n  y: 300,\n  width: 200,\n  height: 60,\n});\n\n// 4. Append a certificate of completion page\nawait embedCertificatePage(pdf, {\n  documentTitle: 'Service Agreement',\n  envelopeId: 'env-2026-0001',\n  completedAt: '2026-08-21T15:30:00Z',\n  signers: [\n    {\n      name: 'Jane Doe',\n      email: 'jane@example.com',\n      role: 'client',\n      signedAt: '2026-08-21T15:29:55Z',\n      ip: '203.0.113.42',\n      userAgent: 'Mozilla/5.0...',\n    },\n  ],\n  integrityHash: 'a1b2c3d4e5f6...',\n});\n\n// 5. Finalize and save\nconst signedBytes = await finalizeSignedPdf(pdf);\nawait fs.writeFile('agreement-signed.pdf', signedBytes);\n```\n\n---\n\n## API reference\n\n### `loadPdf(bytes)`\n\nLoads a PDF document from bytes.\n\n```typescript\nconst pdf = await loadPdf(new Uint8Array(fileBuffer));\n```\n\n- `bytes` — `Uint8Array` of the PDF file content\n- Returns `Promise<PDFDocument>` — a `pdf-lib` document instance\n\n---\n\n### `embedFieldValues(pdf, fields, values, options?)`\n\nDraws text values at specified coordinates on the PDF. Skips fields whose value is\n`undefined`, `null`, or empty after sanitization.\n\n```typescript\nawait embedFieldValues(\n  pdf,\n  [\n    {\n      key: 'clientName',        // matches a key in `values`\n      type: 'text',              // optional field type\n      role: 'client',            // optional role (for per-signer filtering)\n      page: 1,                   // 1-indexed page number\n      x: 100,                    // top-left origin X\n      y: 200,                    // top-left origin Y\n      width: 250,                // default: 200\n      height: 14,                // default: 14\n      fontSize: 11,              // default: 10\n      color: { r: 0, g: 0, b: 0 }, // default: black\n    },\n  ],\n  { clientName: 'Jane Doe' },\n  { font: customFont }           // optional; defaults to Helvetica\n);\n```\n\n**Parameters:**\n- `pdf` — `PDFDocument` (from `loadPdf` or `pdf-lib`)\n- `fields` — array of `PdfOverlayField` (see [Types](#types))\n- `values` — `Record<string, unknown>` — values keyed by field `key`; non-string values\n  are coerced via `String()`\n- `options.font` — optional `PDFFont` (defaults to `StandardFonts.Helvetica`)\n\n**Coordinate system:** fields use **top-left origin** (y increases downward). The function\nconverts to `pdf-lib`'s bottom-left origin internally. See [Coordinate system](#coordinate-system).\n\n**Throws:** if the page number is out of range.\n\n---\n\n### `embedSignatureImage(pdf, imageBase64, overlay)`\n\nEmbeds a PNG signature image at the specified position.\n\n```typescript\nawait embedSignatureImage(pdf, 'data:image/png;base64,iVBORw0KGgo...', {\n  page: 1,\n  x: 100,\n  y: 300,\n  width: 200,\n  height: 60,\n});\n```\n\n**Parameters:**\n- `pdf` — `PDFDocument`\n- `imageBase64` — base64-encoded PNG. Accepts both:\n  - `data:image/png;base64,...` (data URI prefix)\n  - raw base64 string (no prefix)\n- `overlay` — `SignatureOverlay` with `page`, `x`, `y` (top-left origin), `width`, `height`\n\n**Throws:**\n- `TypeError('Invalid base64 PNG signature image')` — if the base64 is malformed\n- `TypeError('Signature image must be a valid PNG')` — if the bytes are not a valid PNG\n- `Error('Page N does not exist...')` — if the page number is out of range\n\n---\n\n### `embedCertificatePage(pdf, data, options?)`\n\nAppends a \"Certificate of Completion\" page to the PDF with signer details and an\nintegrity hash.\n\n```typescript\nawait embedCertificatePage(\n  pdf,\n  {\n    documentTitle: 'Service Agreement',\n    envelopeId: 'env-2026-0001',\n    completedAt: '2026-08-21T15:30:00Z',\n    signers: [\n      {\n        name: 'Jane Doe',\n        email: 'jane@example.com',\n        role: 'client',\n        signedAt: '2026-08-21T15:29:55Z',\n        ip: '203.0.113.42',\n        userAgent: 'Mozilla/5.0...',\n      },\n      {\n        name: 'John Smith',\n        email: 'john@company.com',\n        role: 'company',\n        signedAt: '2026-08-21T15:30:00Z',\n      },\n    ],\n    integrityHash: 'a1b2c3d4e5f6...',\n  },\n  { font: customFont, boldFont: customBoldFont }\n);\n```\n\n**Parameters:**\n- `pdf` — `PDFDocument`\n- `data` — `CertificatePageData` (see [Types](#types))\n- `options.font` — optional regular font (defaults to `StandardFonts.Helvetica`)\n- `options.boldFont` — optional bold font (defaults to `StandardFonts.HelveticaBold`)\n\n**Certificate page layout:**\n- Title: \"Certificate of Completion\" (18pt bold)\n- Document title (12pt)\n- Envelope ID (12pt)\n- Completed At timestamp (12pt)\n- \"Signers:\" heading (14pt bold)\n- Per signer: name, email, role (10pt) + signed-at + optional IP + optional User-Agent (10pt)\n- Integrity Hash (SHA-256) (10pt)\n\nAll text is WinAnsi-sanitized before drawing.\n\n---\n\n### `finalizeSignedPdf(pdf)`\n\nSaves the modified PDF document to bytes.\n\n```typescript\nconst signedBytes = await finalizeSignedPdf(pdf);\n```\n\n- `pdf` — `PDFDocument` (after all overlays and certificate page are embedded)\n- Returns `Promise<Uint8Array>` — the finalized PDF bytes, ready to write to a file or\n  upload to storage\n\n---\n\n### `sanitizeWinAnsi(text)`\n\nSanitizes a string for `pdf-lib`'s standard fonts (Helvetica, Times, Courier), which use\nWinAnsi encoding and cannot render most Unicode characters.\n\n```typescript\nconst clean = sanitizeWinAnni('Jane \"the client\" Doe — Toronto…');\n// 'Jane \"the client\" Doe - Toronto...'\n```\n\n**Replacements:**\n| Unicode | Replacement |\n|---|---|\n| `'` (U+2018 left single quote) | `'` |\n| `'` (U+2019 right single quote) | `'` |\n| `\"` (U+201C left double quote) | `\"` |\n| `\"` (U+201D right double quote) | `\"` |\n| `–` (U+2013 en dash) | `-` |\n| `—` (U+2014 em dash) | `-` |\n| `…` (U+2026 ellipsis) | `...` |\n\n**Control characters:** all control characters (code < 32) are dropped **except** tab (9),\nline feed (10), and carriage return (13).\n\n**Characters > 255:** replaced with the mapping above, or a space if no mapping exists.\n\nThis function is called automatically by `embedFieldValues` and `embedCertificatePage`.\nYou only need to call it directly if you're drawing text with `pdf-lib` directly and want\nthe same sanitization.\n\n---\n\n### Re-exports\n\nFor convenience, the package re-exports common `pdf-lib` symbols so you don't need a\nseparate import:\n\n```typescript\nimport { PDFDocument, PDFPage, PDFFont, StandardFonts, rgb } from '@clocklobster/signing-pdf';\n```\n\n| Symbol | Source | Use |\n|---|---|---|\n| `PDFDocument` | `pdf-lib` | Document class (for `pdf.embedFont()`, `pdf.addPage()`, etc.) |\n| `PDFPage` | `pdf-lib` | Page class |\n| `PDFFont` | `pdf-lib` | Font class |\n| `StandardFonts` | `pdf-lib` | Enum of built-in fonts (Helvetica, HelveticaBold, etc.) |\n| `rgb` | `pdf-lib` | Color helper — `rgb(r, g, b)` with 0–1 floats |\n\n---\n\n### Types\n\n```typescript\n// Field type — used by higher layers for per-signer filtering and variable resolution\ntype PdfFieldType = 'text' | 'textarea' | 'signature' | 'fillableDate' | 'variable' | 'static';\n\n// A field to overlay on the PDF\ninterface PdfOverlayField {\n  key: string;                    // matches a key in the values map\n  type?: PdfFieldType;\n  role?: string;                  // role that owns/fills this field\n  variable?: string;              // for 'variable' fields: the variable to resolve\n  page: number;                   // 1-indexed page number\n  x: number;                      // top-left origin X\n  y: number;                      // top-left origin Y\n  width?: number;                 // default: 200\n  height?: number;                // default: 14\n  fontSize?: number;              // default: 10\n  color?: { r: number; g: number; b: number }; // 0-1 floats; default: black\n  minDate?: string;               // for 'fillableDate' fields\n  maxDate?: string;               // for 'fillableDate' fields\n  displayFormat?: string;         // for 'fillableDate' fields\n}\n\n// Signature image placement\ninterface SignatureOverlay {\n  page: number;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\n// A signer entry on the certificate page\ninterface CertificateSignerEntry {\n  name: string;\n  email: string;\n  role: string;\n  signedAt: string;\n  ip?: string;\n  userAgent?: string;\n}\n\n// Data for the certificate of completion page\ninterface CertificatePageData {\n  documentTitle: string;\n  envelopeId: string;\n  completedAt: string;\n  signers: CertificateSignerEntry[];\n  integrityHash: string;\n}\n```\n\n---\n\n## Coordinate system\n\n`pdf-lib` uses a **bottom-left origin** (y increases upward), which is the PDF standard.\nHowever, most document templates and visual editors use a **top-left origin** (y increases\ndownward), which matches how humans read documents.\n\nThis package uses **top-left origin** for all field and signature coordinates. The\nconversion to `pdf-lib`'s bottom-left origin is handled internally:\n\n```\npdfY = pageHeight - fieldY - fieldHeight\n```\n\nSo when you specify a field at `{ x: 100, y: 200 }`, it appears 100 points from the left\nedge and 200 points from the **top** of the page.\n\n---\n\n## WinAnsi sanitization\n\n`pdf-lib`'s standard fonts (Helvetica, Times, Courier, and their bold/italic variants)\nuse **WinAnsi encoding**, which covers the Latin-1 character range (0–255). Characters\noutside this range — including common Unicode like smart quotes, em dashes, and ellipses —\ncause `pdf-lib` to throw `Error: WinAnsi encoding does not support this character`.\n\n`sanitizeWinAnni()` replaces the most common problematic Unicode characters with their\nASCII equivalents and strips control characters. It is called automatically by\n`embedFieldValues` and `embedCertificatePage`, so you usually don't need to call it\ndirectly.\n\nIf you embed custom fonts (e.g. a Unicode TrueType font via `pdf.embedFont(ttfBytes)`),\nthose fonts support full Unicode and you do **not** need sanitization — but this package\nsanitizes regardless, which is safe (the replacements are idempotent for ASCII input).\n\n---\n\n## Field types\n\nFields carry an optional `type` for use by higher layers (this package doesn't enforce\ntypes — it draws all non-empty values as text):\n\n| Type | Meaning | Drawn as |\n|---|---|---|\n| `text` | Single-line text input | One line of text |\n| `textarea` | Multi-line text input | One line (no wrapping logic — caller splits lines) |\n| `signature` | Signature pad area | Use `embedSignatureImage` instead |\n| `fillableDate` | Date input with min/max/format | Text (the resolved date string) |\n| `variable` | Resolved from a variable map | Text (the resolved value) |\n| `static` | Non-fillable label | Text (if a value is provided) |\n\nThe `role` field is used by higher layers for per-signer filtering (e.g. only show\nfields belonging to the current signer). This package ignores it — it draws all fields\nwhose `key` has a non-empty value.\n\n---\n\n## Certificate page\n\nThe certificate page is appended as the **last page** of the PDF. It includes:\n\n- **Title**: \"Certificate of Completion\" (18pt bold)\n- **Document title**: the `documentTitle` from `CertificatePageData` (12pt)\n- **Envelope ID**: the `envelopeId` (12pt)\n- **Completed At**: the `completedAt` timestamp (12pt)\n- **Signers**: for each signer in `signers[]`:\n  - Name, email, role (10pt)\n  - Signed-at timestamp (10pt)\n  - IP address (10pt, if provided)\n  - User-Agent (10pt, if provided)\n- **Integrity Hash**: the SHA-256 `integrityHash` (10pt)\n\nAll text is WinAnsi-sanitized before drawing. The page uses 50-point margins.\n\n---\n\n## Testing\n\nThe suite uses [Vitest](https://vitest.dev/) and tests against real `pdf-lib` documents.\n8 tests across 4 describe blocks:\n\n| Describe block | Tests | Coverage |\n|---|---|---|\n| `embedCertificatePage` | 2 | Certificate page layout, signer details, integrity hash |\n| `embedFieldValues` | 2 | Field text embedding, coordinate conversion, skip empty values |\n| `embedSignatureImage` | 2 | PNG embedding, base64 data URI parsing, invalid PNG error |\n| `sanitizeWinAnsi` | 2 | Unicode replacement, control character stripping |\n\n```bash\nnpm test             # vitest run (real pdf-lib documents, no mocks)\n```\n\n---\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Typecheck\npnpm run typecheck    # tsc --noEmit\n\n# Run tests\npnpm test             # vitest run\n\n# Build (emit to dist/)\npnpm run build        # tsc -p tsconfig.build.json\n```\n\n### Requirements\n\n- Node.js >= 18\n- pnpm (or npm/yarn)\n- TypeScript >= 5\n\n---\n\n## Project layout\n\n```text\nsigning-pdf/\n├── src/\n│   ├── index.ts      # Public exports + pdf-lib re-exports\n│   ├── load.ts       # loadPdf — load a PDF from bytes\n│   ├── overlay.ts    # embedFieldValues, embedSignatureImage, embedCertificatePage + types\n│   ├── finalize.ts   # finalizeSignedPdf — save to bytes\n│   └── sanitize.ts   # sanitizeWinAnsi — WinAnsi encoding sanitization\n├── test/\n│   ├── certificate.test.ts  # Certificate page tests\n│   ├── overlay.test.ts      # Field + signature overlay tests\n│   └── sanitize.test.ts     # WinAnsi sanitization tests\n├── package.json\n├── tsconfig.json\n├── tsconfig.build.json\n├── LICENSE\n└── README.md\n```\n\n---\n\n## Usage in an e-signature flow\n\nThis package handles the PDF layer. A complete e-signature flow typically also needs:\n\n1. **Envelope lifecycle** — create, track, and complete signing envelopes (not included)\n2. **Storage** — store the template PDF and the finalized signed PDF (S3, local FS, etc.)\n   (not included)\n3. **Email** — send signing invitations and completion notifications (not included)\n4. **Web UI** — a signature pad and field form for the signer (not included)\n5. **Webhook/API** — receive signing events and trigger finalization (not included)\n\nThis package handles step 5's PDF finalization (load → embed fields → embed signatures →\nappend certificate → finalize) and nothing else. The orchestration, storage, email, and\nUI layers are the consuming application's responsibility.\n\n---\n\n## Contributing\n\nPull requests are welcome.\n\n### Guidelines\n\n1. Add or update tests for any change (Vitest, real `pdf-lib` documents).\n2. Ensure `pnpm run typecheck` and `pnpm test` pass.\n3. Do not commit secrets, `.env` files, or `dist/` output.\n4. Follow the existing code style (strict TypeScript, no `any`, pure functions).\n5. Keep the package low-level — no storage, email, or envelope lifecycle concerns.\n\n---\n\n## License\n\n[MIT](LICENSE) © Victor Salmon\n","readmeFilename":"README.md","_rev":"1-2c95f1600f1881ea91213e052e88c379"}