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

secure-pdf

v1.0.1

Published

Production-grade secure PDF generation with encryption, watermarking, tamper detection, expiration, forensic tracking, and QR verification.

Readme

secure-pdf

npm version License: MIT GitHub

Node.js package for generating highly secure PDFs. Combines AES-256 encryption, granular permissions, visible and forensic watermarking, self-destruct expiration, view tracking, QR-based authenticity verification, and cryptographic tamper detection into a single fluent API.


Features

  • AES-256 Encryption — password-protect PDFs with user and owner passwords
  • Granular Permissions — control printing, copying, modifying, annotating per-document
  • Watermarks — visible diagonal watermarks with full style control
  • Self-Destruct — documents expire after a specified date
  • View Tracking — embed unique IDs to trace document distribution
  • Forensic Watermarks — invisible steganographic patterns for leak detection
  • QR Verification — embedded QR codes linking to verification endpoints
  • Tamper Detection — SHA-256 content hashing with integrity verification
  • Metadata — title, author, classification, keywords, custom fields
  • Tables & Images — built-in table renderer, image embedding, custom fonts
  • Fluent API — chain all operations in a clean, readable style
  • TypeScript Ready — complete .d.ts type definitions included
  • Minimal Dependencies — only pdfkit and qrcode

Installation

npm install secure-pdf

Requires Node.js 18 or later.


Quick Start

import { SecurePDF } from 'secure-pdf';

const pdf = new SecurePDF();
pdf
  .addText('Hello, Secure World!', { fontSize: 24, bold: true })
  .watermark('DRAFT')
  .encrypt('my-password');

await pdf.save('document.pdf');

Usage Examples

Basic PDF with Encryption

import { SecurePDF } from 'secure-pdf';

const pdf = new SecurePDF();

pdf
  .addText('Quarterly Report', { fontSize: 28, bold: true })
  .moveDown(1)
  .addText('Revenue exceeded targets across all business units.')
  .addTable(
    [
      ['Region', 'Revenue', 'Growth'],
      ['North America', '$42M', '+18%'],
      ['Europe', '$28M', '+12%'],
      ['APAC', '$15M', '+31%'],
    ],
    { headerBackground: '#1a1a2e', headerColor: '#ffffff' }
  )
  .watermark('CONFIDENTIAL')
  .encrypt('report-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({
    title: 'Q4 Report',
    author: 'Finance Team',
    classification: 'CONFIDENTIAL',
  });

await pdf.save('quarterly-report.pdf');

All Security Features

import { SecurePDF } from 'secure-pdf';

const pdf = new SecurePDF();

await pdf
  .addPage()
  .addText('Confidential Report', { fontSize: 24, bold: true })
  .addTable([
    ['Name', 'Department', 'Clearance'],
    ['John Doe', 'Engineering', 'Level 3'],
  ])
  .watermark('CONFIDENTIAL', {
    opacity: 0.3,
    angle: 45,
    fontSize: 72,
  })
  .permissions({
    printing: 'lowResolution',
    modifying: false,
    copying: false,
    annotating: false,
  })
  .encrypt('SecurePass2024!')
  .metadata({
    title: 'Q4 Financial Report',
    author: 'Finance Team',
    subject: 'Confidential',
    keywords: ['finance', 'Q4', 'confidential'],
    classification: 'INTERNAL USE ONLY',
  })
  .expire('2026-12-31')
  .track('USER-12345-DOC-789')
  .forensicWatermark({ userId: 'jdoe', timestamp: true })
  .qrVerify('https://verify.company.com/check/')
  .tamperDetect(true)
  .save('confidential-report.pdf');

Enterprise Document

import { SecurePDF } from 'secure-pdf';

const recipientId = 'EXEC-CFO-2026';
const pdf = new SecurePDF({
  pageSize: 'Letter',
  margins: { top: 60, bottom: 60, left: 55, right: 55 },
});

pdf
  .addText('Annual Financial Summary', { fontSize: 32, bold: true, align: 'center' })
  .moveDown(2)
  .addText('CONFIDENTIAL — EXECUTIVE DISTRIBUTION ONLY', {
    fontSize: 12, bold: true, color: '#c0392b', align: 'center',
  })
  .addPage()
  .addText('Financial Summary', { fontSize: 22, bold: true })
  .addTable([
    ['Category', 'FY2024', 'FY2025', 'Change'],
    ['Revenue', '$142.3M', '$168.7M', '+18.6%'],
    ['Net Income', '$34.8M', '$49.4M', '+41.9%'],
  ])
  .watermark('CONFIDENTIAL', { fontSize: 60, opacity: 0.1 })
  .encrypt('AcmeFY2025!')
  .expire('2027-03-31')
  .track(recipientId)
  .forensicWatermark({ userId: recipientId, timestamp: true })
  .qrVerify('https://docs.acmecorp.internal/verify/')
  .tamperDetect('acme-fy2025-integrity');

const result = await pdf.save('enterprise-report.pdf');
console.log(`Hash: ${result.contentHash}`);
console.log(`Tracking: ${result.trackingId}`);
console.log(`Document ID: ${result.documentId}`);

Get Buffer (No File)

const { buffer, contentHash, trackingId, documentId } = await pdf.toBuffer();
// Use for HTTP responses, S3 uploads, email attachments, etc.

Database Integration

secure-pdf does not connect to databases itself — you query your database first, then pass the data into the PDF builder. The core pattern is always the same regardless of which database you use:

Query DB  →  Map results to string[][]  →  Pass to .addTable()

The secure-pdf code is identical across all databases — only the query syntax changes.


PostgreSQL (pg)

import { SecurePDF } from 'secure-pdf';
import pg from 'pg';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// 1. Fetch rows from PostgreSQL
const { rows } = await pool.query(
  'SELECT name, department, salary FROM employees ORDER BY department'
);

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...rows.map(r => [r.name, r.department, `$${r.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

MySQL (mysql2)

import { SecurePDF } from 'secure-pdf';
import mysql from 'mysql2/promise';

const pool = mysql.createPool({ host: 'localhost', user: 'root', database: 'mydb' });

// 1. Fetch rows from MySQL
const [rows] = await pool.query(
  'SELECT name, department, salary FROM employees ORDER BY department'
);

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...rows.map(r => [r.name, r.department, `$${r.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

SQLite (better-sqlite3)

import { SecurePDF } from 'secure-pdf';
import Database from 'better-sqlite3';

const db = new Database('mydb.sqlite');

// 1. Fetch rows from SQLite (synchronous)
const rows = db.prepare(
  'SELECT name, department, salary FROM employees ORDER BY department'
).all();

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...rows.map(r => [r.name, r.department, `$${r.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

MongoDB (mongodb)

import { SecurePDF } from 'secure-pdf';
import { MongoClient } from 'mongodb';

const client = new MongoClient(process.env.MONGO_URI);
await client.connect();
const db = client.db('mydb');

// 1. Fetch documents from MongoDB
const employees = await db.collection('employees')
  .find({})
  .sort({ department: 1 })
  .toArray();

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...employees.map(e => [e.name, e.department, `$${e.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

Note: MongoDB returns plain objects (documents), not rows. Access fields directly — e.name, e.department. For nested fields use dot access: e.address.city. Use e._id.toString() when passing to .track().


Mongoose

import { SecurePDF } from 'secure-pdf';
import mongoose from 'mongoose';

await mongoose.connect(process.env.MONGO_URI);

const Employee = mongoose.model('Employee', new mongoose.Schema({
  name: String,
  department: String,
  salary: Number,
}));

// 1. Fetch documents using Mongoose
const employees = await Employee.find().sort({ department: 1 }).lean();

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...employees.map(e => [e.name, e.department, `$${e.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

Tip: Use .lean() on Mongoose queries to get plain JavaScript objects instead of Mongoose documents — this makes field access faster and more predictable.


Prisma

import { SecurePDF } from 'secure-pdf';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// 1. Fetch records using Prisma
const employees = await prisma.employee.findMany({
  orderBy: { department: 'asc' },
  select: { name: true, department: true, salary: true },
});

// 2. Build the PDF — same for every database
const pdf = new SecurePDF();

pdf
  .addText('Employee Report', { fontSize: 24, bold: true })
  .moveDown(1)
  .addText(`Generated: ${new Date().toLocaleDateString()}`, { fontSize: 11, color: '#888888' })
  .moveDown(1)
  .addTable([
    ['Name', 'Department', 'Salary'],
    ...employees.map(e => [e.name, e.department, `$${e.salary.toLocaleString()}`]),
  ])
  .watermark('CONFIDENTIAL')
  .encrypt('hr-password-2026')
  .permissions({ copying: false, modifying: false })
  .metadata({ title: 'Employee Report', author: 'HR System', classification: 'INTERNAL' });

const { buffer } = await pdf.toBuffer();

Sending a PDF as an HTTP Response

This pattern works with any of the database clients above — just swap the query section:

import express from 'express';
import { SecurePDF } from 'secure-pdf';

const app = express();

app.get('/reports/invoices/:id', async (req, res) => {
  // Swap this block for whichever DB client you use
  const rows = await yourDb.getInvoiceItems(req.params.id);

  if (!rows.length) return res.status(404).send('Invoice not found');

  const pdf = new SecurePDF();

  pdf
    .addText(`Invoice #${req.params.id}`, { fontSize: 26, bold: true })
    .moveDown(1)
    .addTable([
      ['Item', 'Qty', 'Unit Price', 'Total'],
      ...rows.map(r => [r.description, String(r.qty), `$${r.unit_price}`, `$${r.total}`]),
    ])
    .metadata({ title: `Invoice #${req.params.id}`, author: 'Billing System' })
    .encrypt('invoice-2026');

  const { buffer } = await pdf.toBuffer();

  res.set({
    'Content-Type': 'application/pdf',
    'Content-Disposition': `attachment; filename="invoice-${req.params.id}.pdf"`,
    'Content-Length': buffer.length,
  });
  res.send(buffer);
});

Per-User Document Tracking

Combine .track() and .forensicWatermark() with a DB-stored user ID to trace distributed documents back to their recipient. Works with any database — just fetch the user record first:

// PostgreSQL / MySQL / SQLite: recipient = user.rows[0] or rows[0]
// MongoDB / Mongoose: recipient = await User.findById(userId).lean()
// Prisma: recipient = await prisma.user.findUnique({ where: { id: userId } })

const pdf = new SecurePDF();

pdf
  .addText(`Welcome, ${recipient.name}`, { fontSize: 20, bold: true })
  .addText('This document is personalized and tracked.', { fontSize: 12 })
  .track(`USER-${recipient.id ?? recipient._id.toString()}`)
  .forensicWatermark({ userId: recipient.email, timestamp: true })
  .encrypt('access-password')
  .tamperDetect(true);

const { buffer, trackingId } = await pdf.toBuffer();

// Optionally log the tracking ID back to the database
// e.g. await db.query('INSERT INTO document_log ...') or
//      await prisma.documentLog.create({ data: { userId, trackingId } })

Supported Database Clients

| Database | Client | |------------|-------------------------------------| | PostgreSQL | pg, postgres | | MySQL | mysql2 | | SQLite | better-sqlite3 | | MongoDB | mongodb, mongoose | | Any ORM | prisma, drizzle, sequelize |


API Reference

Constructor

new SecurePDF(options?)

| Option | Type | Default | Description | |---|---|---|---| | documentId | string | auto-generated | Custom document identifier | | pageSize | string | 'A4' | Page size ('A4', 'Letter', etc.) | | margins | object | { top: 50, bottom: 50, left: 50, right: 50 } | Page margins in points |

Instance Properties

| Property | Type | Description | |---|---|---| | documentId | string | The unique identifier for this document instance |

Content Methods

All content methods return this for chaining.

| Method | Parameters | Description | |---|---|---| | addText(text, options?) | text: string, options: TextOptions | Add a text block | | addPage(options?) | options: object | Insert a page break | | addImage(source, options?) | source: string \| Buffer, options: ImageOptions | Embed an image | | addTable(rows, options?) | rows: string[][], options: TableOptions | Render a table (first row = header) | | setFont(name, path) | name: string, path: string | Register and use a custom font | | moveDown(lines?) | lines: number | Add vertical spacing |

TextOptions

| Property | Type | Description | |---|---|---| | fontSize | number | Font size in points | | bold | boolean | Use bold weight | | italic | boolean | Use italic style | | underline | boolean | Underline text | | color | string | Fill color (hex) | | align | 'left' \| 'center' \| 'right' \| 'justify' | Text alignment | | font | string | Font name override | | link | string | Hyperlink URL | | width | number | Maximum width for text wrapping (points) | | lineBreak | boolean | Enable or disable automatic line breaks | | indent | number | Left indent in points |

ImageOptions

| Property | Type | Description | |---|---|---| | x | number | Absolute x position | | y | number | Absolute y position | | width | number | Image width in points | | height | number | Image height in points | | fit | [number, number] | Fit image within [width, height] bounds | | align | 'left' \| 'center' \| 'right' | Horizontal alignment | | valign | 'top' \| 'center' \| 'bottom' | Vertical alignment |

TableOptions

| Property | Type | Default | Description | |---|---|---|---| | fontSize | number | 10 | Font size for all cells | | columnWidth | number | auto (equal split) | Fixed width per column in points | | headerBackground | string | '#eeeeee' | Header row background color (hex) | | headerColor | string | '#333333' | Header row text color (hex) | | cellColor | string | '#000000' | Body cell text color (hex) | | borderColor | string | '#cccccc' | Cell border color (hex) |

Security Methods

| Method | Parameters | Description | |---|---|---| | encrypt(password) | password: string | AES-256 encryption with user password | | permissions(config) | config: PermissionsConfig | Set document permissions | | metadata(config) | config: MetadataConfig | Set document metadata | | watermark(text, options?) | text: string, options: WatermarkOptions | Visible watermark on every page | | expire(date) | date: string \| Date | Self-destruct after date | | track(id) | id: string | Embed tracking identifier | | forensicWatermark(config?) | config: ForensicConfig | Hidden forensic watermark | | qrVerify(url, options?) | url: string, options: QROptions | Embed verification QR code | | tamperDetect(enableOrSalt?) | boolean \| string | Enable content integrity hashing |

PermissionsConfig

| Property | Type | Values | |---|---|---| | printing | string \| boolean | 'highResolution', 'lowResolution', true, false | | modifying | boolean | Allow content modification | | copying | boolean | Allow content copying | | annotating | boolean | Allow annotations | | fillingForms | boolean | Allow form filling | | contentAccessibility | boolean | Allow accessibility extraction | | documentAssembly | boolean | Allow page assembly |

MetadataConfig

| Property | Type | Description | |---|---|---| | title | string | Document title | | author | string | Document author | | subject | string | Document subject | | keywords | string \| string[] | Search keywords | | creator | string | Creating application name | | producer | string | PDF producer name | | company | string | Organization name | | classification | string | Security classification label |

WatermarkOptions

| Property | Type | Default | Description | |---|---|---|---| | fontSize | number | 60 | Watermark font size | | color | string | '#cccccc' | Watermark color | | opacity | number | 0.15 | Opacity (0–1) | | angle | number | -45 | Rotation in degrees | | font | string | 'Helvetica' | Font name |

QROptions

| Property | Type | Description | |---|---|---| | x | number | Absolute x position of the QR code | | y | number | Absolute y position of the QR code | | size | number | QR code size in points |

ForensicConfig

| Property | Type | Description | |---|---|---| | userId | string | User identifier to embed in forensic pattern | | timestamp | boolean | Include generation timestamp in pattern |

Output Methods

| Method | Returns | Description | |---|---|---| | save(filePath) | Promise<SaveResult> | Write PDF to disk | | toBuffer() | Promise<BufferResult> | Get PDF as in-memory Buffer |

SaveResult

{
  path: string;          // Absolute file path
  size: number;          // File size in bytes
  documentId: string;    // Document identifier
  contentHash: string;   // SHA-256 hash (if tamperDetect enabled)
  trackingId: string;    // Tracking ID (if track enabled)
}

BufferResult

{
  buffer: Buffer;        // Raw PDF bytes
  contentHash: string;   // SHA-256 hash (if tamperDetect enabled)
  trackingId: string;    // Tracking ID (if track enabled)
  documentId: string;    // Document identifier
}

Security Features

Encryption

Uses pdfkit's built-in AES-256 encryption. A user password is required to open the document. An owner password is auto-generated with cryptographically secure random bytes and controls permission overrides.

Self-Destruct / Expiration

Two-layer approach:

  1. PDF JavaScript — executes on open in Adobe Acrobat and compatible readers; closes the document if expired
  2. Visible notice — red warning at the bottom of the last page for readers that don't execute JavaScript

PDF JavaScript is not supported in all viewers. The visible notice provides fallback coverage.

Tamper Detection

A SHA-256 hash is computed over all text content and embedded in the PDF metadata (ContentHash key). Verify integrity programmatically:

import { verifyContentHash } from 'secure-pdf';

const result = verifyContentHash(
  savedResult.contentHash,
  ['your', 'content', 'parts'],
  'your-salt'
);
console.log(result.valid);   // true or false
console.log(result.message); // "Document integrity verified." or "Document has been tampered with."

View Tracking

Each tracked document gets a unique ID embedded in:

  • PDF metadata (TrackingID info key)
  • Hidden near-invisible watermark grid across every page
  • Invisible selectable text at the page bottom

This enables forensic identification of the source copy if a document is leaked.

Forensic Watermarks

When .forensicWatermark() is called, a grid of nearly invisible text containing the user ID and timestamp is scattered across every page using a staggered pattern. Corner markers provide additional forensic anchors. The pattern is designed to survive most image processing operations while remaining invisible to the naked eye.

QR Verification

A QR code is embedded on the last page that encodes the verification URL with the document's content hash as a query parameter. Recipients can scan to verify authenticity against a server-side verification endpoint.


Error Handling

secure-pdf exports structured error classes for precise error handling:

import {
  SecurePDFError,
  ValidationError,
  EncryptionError,
  BuildError,
  FeatureError,
} from 'secure-pdf';

try {
  await pdf.save('output.pdf');
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(`Invalid input on field "${err.field}": ${err.message}`);
  } else if (err instanceof BuildError) {
    console.error(`PDF generation failed: ${err.message}`);
  } else if (err instanceof EncryptionError) {
    console.error(`Encryption error: ${err.message}`);
  } else {
    throw err;
  }
}

Error Classes

| Class | Code | Description | |---|---|---| | SecurePDFError | base | Base class for all package errors | | ValidationError | — | Invalid input; includes field property | | EncryptionError | — | Encryption configuration failure | | BuildError | — | PDF generation or rendering failure | | FeatureError | — | Feature-specific failure; includes feature property |


Exported Utilities

In addition to the main class, the package exports low-level utilities for advanced use:

Crypto Utilities

import { sha256, sha256Multi, hmacSha256, generateDocumentId } from 'secure-pdf';

sha256('some data');                        // → hex string
sha256Multi(['part1', 'part2'], 'salt');    // → hex string
hmacSha256('secret-key', 'data');          // → hex string
generateDocumentId();                       // → unique document ID string

Feature Functions

import {
  verifyContentHash,
  applyEncryption,
  buildPermissionFlags,
  renderWatermark,
  applyMetadata,
  applyExpiration,
  embedTracking,
  renderForensicWatermark,
  renderQRVerification,
  computeContentHash,
  embedContentHash,
} from 'secure-pdf';

These are exported for advanced integrations or custom PDF builders using PDFBuilder directly.


TypeScript

The package includes complete type definitions. No additional @types package is needed.

import { SecurePDF, type SaveResult, type PermissionsConfig, type BufferResult } from 'secure-pdf';

const perms: PermissionsConfig = {
  printing: 'lowResolution',
  copying: false,
};

const pdf = new SecurePDF();
pdf.addText('Typed document').permissions(perms);

const result: SaveResult = await pdf.save('typed.pdf');
const { buffer }: BufferResult = await pdf.toBuffer();

Testing

npm test                  # Run all tests
npm run test:coverage     # Run with coverage report

Tests are structured as:

  • tests/unit/ — isolated tests for each feature module
  • tests/integration/ — end-to-end workflow tests

Examples

npm run example:basic       # Simple encrypted PDF
npm run example:advanced    # All security features
npm run example:enterprise  # Corporate document workflow

Output is written to examples/output/.


Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Write tests for new functionality
  4. Ensure npm test passes
  5. Commit with clear messages using conventional commits
  6. Open a Pull Request

Development Setup

git clone https://github.com/CodeByMoriarty/secure-pdf.git
cd secure-pdf
npm install
npm test

License

MIT


Support

Open an issue at the GitHub repository for bug reports, feature requests, or questions.

You can also reach the maintainer by email: [email protected]

For security vulnerabilities, please see SECURITY.md.