{"_id":"@abineshsolairaj/pdf-merge","_rev":"8-c7bf62c04a44ecf1c9b5bb7247cd5948","name":"@abineshsolairaj/pdf-merge","dist-tags":{"latest":"1.5.0"},"versions":{"1.0.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.0.0","keywords":["pdf","merge","pdf-merge","base64","pdf-lib","typescript"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.0.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/abineshsolairaj/pdf-merge#readme","bugs":{"url":"https://github.com/abineshsolairaj/pdf-merge/issues"},"dist":{"shasum":"bf72984e74f9eb84f2b79a4a9b1f8373db0858f7","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.0.0.tgz","fileCount":5,"integrity":"sha512-u9eSxoMd+Dz1oESxP+t0i4Rd4mIWInFxNooPTPzmJblgtHLnYF8KO3dql33HO+KlqPMfyX/RjIaTXEHKObfQvA==","signatures":[{"sig":"MEYCIQCOd1A4yi/5ymLZX/I9fjwEvCTmiNv6JADzznEERL/2fQIhAOFLjcM3of4i+YRJtc6HJue388Xqi3Rsy4MS5fQcDrdO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":12572},"main":"dist/pdfMerger.js","types":"dist/pdfMerger.d.ts","engines":{"node":">=18"},"gitHead":"106da99de527e84d4a0f0a24d23ceb6135a7fe66","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"tsc","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/abineshsolairaj/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings or URLs, preserving input order.","directories":{},"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.0.0_1781069424967_0.7369057939248258","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.1.0","keywords":["pdf","merge","pdf-merge","base64","pdf-lib","typescript"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.1.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/abineshsolairaj/pdf-merge#readme","bugs":{"url":"https://github.com/abineshsolairaj/pdf-merge/issues"},"dist":{"shasum":"01ae11a5ebe8be7e3296aedb81868d59ffc27a88","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.1.0.tgz","fileCount":5,"integrity":"sha512-+4yi4qvaJklfBtwGcL27yw/cEGSJLc7F3c4BrBWYYrWIT9Wx05TpAgNRoLxKoCDVEnEQ7NJ7M5wo21xL0Q23Uw==","signatures":[{"sig":"MEUCIAqi+PsybvIbZhPUckWX3G49EqLXgjZzjuRYRXSRQIk5AiEA9jdQJI3px1UsIhP965dmmMhwa/6lK5H5LUrvd88sU1c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19414},"main":"dist/pdfMerger.js","types":"dist/pdfMerger.d.ts","engines":{"node":">=18"},"gitHead":"d64994594fe467b579701ce0b7a7ddcc91910c37","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"tsc","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/abineshsolairaj/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings or URLs, preserving input order.","directories":{},"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.1.0_1781486727142_0.6808731352015043","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.2.0","keywords":["pdf","merge","pdf-merge","base64","pdf-lib","typescript"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.2.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/abineshsolairaj/pdf-merge#readme","bugs":{"url":"https://github.com/abineshsolairaj/pdf-merge/issues"},"dist":{"shasum":"3a878a95a095802e75c244d96546761a063846b9","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.2.0.tgz","fileCount":5,"integrity":"sha512-3f+HYeR2liN5vh160O0mL4Wdg04Vmm8/pIYSPBJnUFAAqBSQUeSEp/OKJSmluBhDn8yL8Q6QdoRPKUU3nSkeqg==","signatures":[{"sig":"MEUCIAUR9Ph0oaiW9Joe3wNy6B9g+7brM+IHaeqmBE3j73Z/AiEAjOh45mOf235qq7wPK36HcmopCuysvdakeZU5FxdAYq4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":33138},"main":"dist/pdfMerger.js","types":"dist/pdfMerger.d.ts","engines":{"node":">=18"},"gitHead":"ff13e954c6cb001ea9e611f0477b5f772abf71b4","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"tsc","test:e2e":"ts-node --transpile-only test/e2e.ts","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/abineshsolairaj/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings or URLs, preserving input order.","directories":{},"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.2.0_1781719257959_0.969229920565889","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.3.0","keywords":["pdf","merge","pdf-merge","base64","pdf-lib","typescript","cli","esm"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.3.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/thelucidaquarian/pdf-merge#readme","bugs":{"url":"https://github.com/thelucidaquarian/pdf-merge/issues"},"bin":{"pdf-merge":"dist/cjs/cli.js"},"dist":{"shasum":"b59d7730bba653944a3fdd8efa9eb951008a4689","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.3.0.tgz","fileCount":10,"integrity":"sha512-fvjqXMMiHEk3NNWdadxgV3babivNkqpN06c67w5VtmJULvLmuc3+P3xYMFHqPyxXjn/B0ye3vN2/uVFOwe8V/w==","signatures":[{"sig":"MEQCIGWsX0keBhRYy07ianOYz8XhidgY0hzULgU9lei2rppRAiAE1HuvyDvU0ywZ/ujnDn6dxOE2ABGvaUSmfEFD6JVPgw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":66104},"main":"dist/cjs/pdfMerger.js","types":"dist/cjs/pdfMerger.d.ts","module":"dist/esm/pdfMerger.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/cjs/pdfMerger.d.ts","import":"./dist/esm/pdfMerger.js","default":"./dist/cjs/pdfMerger.js","require":"./dist/cjs/pdfMerger.js"},"./package.json":"./package.json"},"gitHead":"0bc50e517d78bc8a677747a55b1402b4d5e7acd5","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"rm -rf dist && npm run build:cjs && npm run build:esm && node scripts/postbuild.js","test:cli":"npm run build && ts-node --transpile-only test/cli.test.ts","test:e2e":"ts-node --transpile-only test/e2e.ts","build:cjs":"tsc -p tsconfig.json","build:esm":"tsc -p tsconfig.esm.json","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/thelucidaquarian/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings, Buffers, file paths, or URLs, with page selection and document metadata.","directories":{},"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.3.0_1781760617324_0.0639201485467622","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.4.0","keywords":["pdf","merge","pdf-merge","merge-pdf","merge-pdfs","combine-pdf","combine-pdfs","concat-pdf","pdf-merger","pdf-tools","pdf-utils","base64","buffer","url","fetch","page-selection","page-range","metadata","pdf-lib","typescript","esm","commonjs","cli","nodejs","node","ssrf","secure"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.4.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/thelucidaquarian/pdf-merge#readme","bugs":{"url":"https://github.com/thelucidaquarian/pdf-merge/issues"},"bin":{"pdf-merge":"dist/cjs/cli.js"},"dist":{"shasum":"ac0ed9177050c1c51a2276d6bf965e376bdd098b","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.4.0.tgz","fileCount":10,"integrity":"sha512-LKO6aRzCXpKNYZQ9SmX7GpImCqg7mNQjVFC7mUbyADdCelzkAS/fPq0pmxW4xzYDLodJZl+OlJYbMfYdM0bSAw==","signatures":[{"sig":"MEUCIQCLfDj42CdfpDsgvwEsIcKCKwrzKVdGRA2RpqxI55jE4AIgB+9fjvgN9JejmnGgPx2IUk4Maxl8mHQOZrLDSnIByMs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":85687},"main":"dist/cjs/pdfMerger.js","types":"dist/cjs/pdfMerger.d.ts","module":"dist/esm/pdfMerger.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/cjs/pdfMerger.d.ts","import":"./dist/esm/pdfMerger.js","default":"./dist/cjs/pdfMerger.js","require":"./dist/cjs/pdfMerger.js"},"./package.json":"./package.json"},"gitHead":"18ee40ca3a952ecceef25d0339c6493dfd3c4752","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"rm -rf dist && npm run build:cjs && npm run build:esm && node scripts/postbuild.js","test:cli":"npm run build && ts-node --transpile-only test/cli.test.ts","test:e2e":"ts-node --transpile-only test/e2e.ts","build:cjs":"tsc -p tsconfig.json","build:esm":"tsc -p tsconfig.esm.json","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/thelucidaquarian/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings, Buffers, file paths, or URLs, with page selection, document metadata, and optional text watermarks.","directories":{},"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.4.0_1781890483858_0.9331504906073391","host":"s3://npm-registry-packages-npm-production"}},"1.5.0":{"name":"@abineshsolairaj/pdf-merge","version":"1.5.0","keywords":["pdf","merge","pdf-merge","merge-pdf","merge-pdfs","combine-pdf","combine-pdfs","concat-pdf","pdf-merger","pdf-tools","pdf-utils","base64","buffer","url","fetch","page-selection","page-range","metadata","pdf-lib","typescript","esm","commonjs","cli","nodejs","node","ssrf","secure"],"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","_id":"@abineshsolairaj/pdf-merge@1.5.0","maintainers":[{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"}],"homepage":"https://github.com/thelucidaquarian/pdf-merge#readme","bugs":{"url":"https://github.com/thelucidaquarian/pdf-merge/issues"},"bin":{"pdf-merge":"dist/cjs/cli.js"},"dist":{"shasum":"9b7793913449a107b017b11cf2df96e7dc5a95e1","tarball":"https://registry.npmjs.org/@abineshsolairaj/pdf-merge/-/pdf-merge-1.5.0.tgz","fileCount":10,"integrity":"sha512-T4HV0D/OUfjkiUz/YMUtqyLDtK2sWJHgEEwZm25N6UbswW+lL7g5fCbCujuMDFs5vb4UqfxZlYG3ntpZi921CA==","signatures":[{"sig":"MEQCIBX6Db9jpse4RsPqh96QPoxdst8HLgdSx/PKs1KIwENPAiBOFlndrZ2m1Gd5Z/VS19dxhJ8vZYw68K25XSmUzh9PgA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":105543},"main":"dist/cjs/pdfMerger.js","types":"dist/cjs/pdfMerger.d.ts","module":"dist/esm/pdfMerger.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/cjs/pdfMerger.d.ts","import":"./dist/esm/pdfMerger.js","default":"./dist/cjs/pdfMerger.js","require":"./dist/cjs/pdfMerger.js"},"./package.json":"./package.json"},"gitHead":"2bf0c02a1d2349b6d8c3199b3c47ce83e531c485","scripts":{"test":"ts-node --transpile-only test/pdfMerger.test.ts","build":"rm -rf dist && npm run build:cjs && npm run build:esm && node scripts/postbuild.js","test:cli":"npm run build && ts-node --transpile-only test/cli.test.ts","test:e2e":"ts-node --transpile-only test/e2e.ts","build:cjs":"tsc -p tsconfig.json","build:esm":"tsc -p tsconfig.esm.json","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"repository":{"url":"git+https://github.com/thelucidaquarian/pdf-merge.git","type":"git"},"_npmVersion":"11.12.1","description":"Utility to merge PDFs from Base64 strings, Buffers, file paths, or URLs, with page selection, document metadata, watermarks, page numbers, AbortSignal cancellation, and dual ESM/CJS support.","directories":{},"sideEffects":false,"_nodeVersion":"23.10.0","dependencies":{"pdf-lib":"^1.17.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ts-node":"^10.9.2","typescript":"^5.4.5","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/pdf-merge_1.5.0_1782010215435_0.24980690557378726","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-06-10T05:30:24.716Z","modified":"2026-06-26T05:29:12.910Z","1.0.0":"2026-06-10T05:30:25.102Z","1.1.0":"2026-06-15T01:25:27.292Z","1.2.0":"2026-06-17T18:00:58.111Z","1.3.0":"2026-06-18T05:30:17.473Z","1.4.0":"2026-06-19T17:34:44.009Z","1.5.0":"2026-06-21T02:50:15.569Z"},"bugs":{"url":"https://github.com/thelucidaquarian/pdf-merge/issues"},"author":{"name":"abineshsolairaj","email":"abineshsolairaj@gmail.com"},"license":"MIT","homepage":"https://github.com/thelucidaquarian/pdf-merge#readme","keywords":["pdf","merge","pdf-merge","merge-pdf","merge-pdfs","combine-pdf","combine-pdfs","concat-pdf","pdf-merger","pdf-tools","pdf-utils","base64","buffer","url","fetch","page-selection","page-range","metadata","pdf-lib","typescript","esm","commonjs","cli","nodejs","node","ssrf","secure"],"repository":{"url":"git+https://github.com/thelucidaquarian/pdf-merge.git","type":"git"},"description":"Utility to merge PDFs from Base64 strings, Buffers, file paths, or URLs, with page selection, document metadata, watermarks, page numbers, AbortSignal cancellation, and dual ESM/CJS support.","maintainers":[{"email":"abineshsolairaj@gmail.com","name":"abineshsolairaj"}],"readme":"# @abineshsolairaj/pdf-merge\n\n[![CI](https://github.com/thelucidaquarian/pdf-merge/actions/workflows/ci.yml/badge.svg)](https://github.com/thelucidaquarian/pdf-merge/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/@abineshsolairaj/pdf-merge.svg)](https://www.npmjs.com/package/@abineshsolairaj/pdf-merge)\n[![downloads](https://img.shields.io/npm/dm/@abineshsolairaj/pdf-merge.svg)](https://www.npmjs.com/package/@abineshsolairaj/pdf-merge)\n[![types](https://img.shields.io/npm/types/@abineshsolairaj/pdf-merge.svg)](https://www.npmjs.com/package/@abineshsolairaj/pdf-merge)\n[![license](https://img.shields.io/npm/l/@abineshsolairaj/pdf-merge.svg)](./LICENSE)\n[![node](https://img.shields.io/node/v/@abineshsolairaj/pdf-merge.svg)](https://www.npmjs.com/package/@abineshsolairaj/pdf-merge)\n\n> Merge PDFs in Node.js or TypeScript — from Base64, Buffers, file paths, or\n> URLs — with per-input page selection, document metadata, watermarks, page\n> numbers, `AbortSignal` cancellation, SSRF-safe URL fetching, typed errors,\n> a CLI, and dual ESM/CommonJS support. Built on\n> [`pdf-lib`](https://pdf-lib.js.org/).\n\n## Why this library?\n\nMost PDF-merge packages on npm cover the basics — take an array of\nbuffers, concatenate them. `@abineshsolairaj/pdf-merge` adds the\nproduction-grade extras that real applications usually have to bolt on\nthemselves:\n\n- **Four input sources** in one API — Base64, `Buffer` / `Uint8Array`, file\n  paths, and URLs — so you don't have to pre-decode or pre-fetch.\n- **Per-input page selection** via array (`[1, 3, 5]`) or range string\n  (`\"1-3,5,8-10\"`) — pick exactly the pages you want from each source.\n- **Document metadata** — stamp title, author, subject, keywords, creator,\n  and creation/modification dates on the merged output.\n- **Watermarks and page numbers** — overlay configurable text on every\n  page, plus continuous page numbering with `{current}` / `{total}`\n  templates.\n- **`AbortSignal` cancellation** — cancel mid-merge from React effects,\n  request handlers, or batch jobs; in-flight URL fetches abort cleanly.\n- **SSRF-safe URL fetching by default** — protocol allowlist, response size\n  cap, per-request timeout, redirect-protocol checks, bounded concurrency,\n  and URL sanitization in error messages so signed-URL tokens never reach\n  your logs.\n- **Typed errors with input-index correlation** — `instanceof PdfFetchError`\n  tells you exactly which URL failed and gives you the sanitized `.url`\n  and `.index`.\n- **Bundled `pdf-merge` CLI** (see [CLI](#cli) for the recommended\n  invocation forms).\n- **Dual ESM and CommonJS build** — works in modern bundlers, Deno, Bun,\n  and legacy `require()` consumers from a single install.\n- **Order-preserving** — output pages always follow the input array order.\n- **Zero runtime config** — sensible defaults, fully configurable per call.\n\n## Install\n\n```bash\nnpm install @abineshsolairaj/pdf-merge\n```\n\nRequires Node.js 18+ (uses the global `fetch` and `AbortController`).\n\n## Quick start\n\n```ts\nimport {\n  mergeBase64PDFs,\n  mergePdfBuffers,\n  mergePdfFiles,\n  mergePdfUrls,\n} from '@abineshsolairaj/pdf-merge';\n\n// From Base64 strings → Base64 string\nconst b64Out = await mergeBase64PDFs([pdfA_b64, pdfB_b64]);\n\n// From raw bytes → Uint8Array (no Base64 round-trip)\nconst bytesOut = await mergePdfBuffers([bufA, bufB]);\n\n// From disk → Uint8Array\nconst fileOut = await mergePdfFiles(['./a.pdf', './b.pdf']);\n\n// From URLs → Base64 string\nconst urlOut = await mergePdfUrls(['https://example.com/a.pdf', 'https://example.com/b.pdf']);\n\n// With watermark, page numbers, metadata, and cancellation\nconst controller = new AbortController();\nconst annotated = await mergePdfFiles(['./cover.pdf', './body.pdf'], {\n  metadata: { title: 'Annual Report 2026', author: 'Operations' },\n  watermark: { text: 'CONFIDENTIAL', opacity: 0.15, rotate: 45 },\n  pageNumbers: { format: 'Page {current} of {total}' },\n  signal: controller.signal,\n});\n```\n\n## API\n\n### `mergeBase64PDFs(inputs, options?): Promise<string>`\n\nMerges Base64-encoded PDFs and returns the merged document as Base64.\n\n- Accepts plain Base64 or `data:application/pdf;base64,…` prefixed strings.\n- `inputs` is `(string | { data: string; pages?: PageSelector })[]`.\n- `options` accepts `metadata`, `watermark`, `pageNumbers`,\n  `ignoreEncryption`, and `signal`. See [Document metadata](#document-metadata),\n  [Watermark](#watermark), [Page numbers](#page-numbers),\n  [Encrypted source PDFs](#encrypted-source-pdfs), and\n  [Cancellation](#cancellation-with-abortsignal).\n\n```ts\nconst merged = await mergeBase64PDFs([\n  reportA_b64,                                // all pages of A\n  { data: reportB_b64, pages: [1, 3, 5] },    // pages 1, 3, 5 of B\n  { data: reportC_b64, pages: '1-3,7' },      // pages 1, 2, 3, 7 of C\n]);\n```\n\n### `mergePdfBuffers(inputs, options?): Promise<Uint8Array>`\n\nMerges raw PDF bytes (from `fs.readFile`, an S3 SDK, an HTTP body, multer,\netc.) and returns the merged document as a `Uint8Array`. Skips the Base64\nround-trip, saving ~33 % memory vs `mergeBase64PDFs`.\n\n- `inputs` is `(Buffer | Uint8Array | { data: Buffer | Uint8Array; pages?: PageSelector })[]`.\n- `options` accepts `metadata`, `watermark`, `pageNumbers`,\n  `ignoreEncryption`, and `signal`. See [Document metadata](#document-metadata),\n  [Watermark](#watermark), [Page numbers](#page-numbers),\n  [Encrypted source PDFs](#encrypted-source-pdfs), and\n  [Cancellation](#cancellation-with-abortsignal).\n\n```ts\nconst merged = await mergePdfBuffers([bufA, { data: bufB, pages: [4, 2] }]);\n```\n\n### `mergePdfFiles(inputs, options?): Promise<Uint8Array>`\n\nReads PDFs from the given file paths in parallel and merges them. Returns a\n`Uint8Array` — write it straight to disk.\n\n- `inputs` is `(string | { path: string; pages?: PageSelector })[]`.\n- `options` accepts `metadata`, `watermark`, `pageNumbers`,\n  `ignoreEncryption`, and `signal`. See [Document metadata](#document-metadata),\n  [Watermark](#watermark), [Page numbers](#page-numbers),\n  [Encrypted source PDFs](#encrypted-source-pdfs), and\n  [Cancellation](#cancellation-with-abortsignal).\n\n```ts\nimport { promises as fs } from 'fs';\n\nconst merged = await mergePdfFiles([\n  './cover.pdf',\n  { path: './body.pdf', pages: '2-9' },\n]);\nawait fs.writeFile('./out.pdf', merged);\n```\n\n### `mergePdfUrls(urls, options?): Promise<string>`\n\nFetches PDFs from the given URLs (concurrently, with a bounded pool) and\nreturns the merged document as Base64.\n\n`urls` is `(string | { url: string; headers?: Record<string,string>; pages?: PageSelector })[]`.\n\nOptions:\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `timeoutMs` | `5000` | Per-request timeout in milliseconds. |\n| `maxBytesPerUrl` | `100 * 1024 * 1024` | Maximum bytes accepted from any single response. |\n| `allowedProtocols` | `['http:', 'https:']` | URL protocols allowed. Pass `['https:']` to harden further. |\n| `concurrency` | `8` | Maximum number of URL fetches running in parallel. |\n| `headers` | — | Default headers applied to every fetch. Per-URL headers (object form) override on key conflict. |\n| `metadata` | — | Document metadata to stamp on the merged output. See [Document metadata](#document-metadata). |\n| `watermark` | — | Text watermark to draw on every page of the merged output. See [Watermark](#watermark). |\n| `pageNumbers` | — | Stamp continuous page numbers. See [Page numbers](#page-numbers). |\n| `signal` | — | `AbortSignal` to cancel the merge. See [Cancellation](#cancellation-with-abortsignal). |\n| `ignoreEncryption` | `false` | Allow merging source PDFs that declare encryption metadata. See [Encrypted source PDFs](#encrypted-source-pdfs). |\n\n```ts\nconst merged = await mergePdfUrls(\n  [\n    'https://example.com/a.pdf',\n    { url: 'https://example.com/b.pdf', headers: { Authorization: 'Bearer token-b' }, pages: '1-3' },\n  ],\n  {\n    timeoutMs: 8000,\n    maxBytesPerUrl: 20 * 1024 * 1024,\n    allowedProtocols: ['https:'],\n    concurrency: 4,\n    headers: { 'User-Agent': 'my-app/1.0' },\n  },\n);\n```\n\n## Document metadata\n\nEvery merge function accepts an optional second `options` argument with a\n`metadata` field. Stamp the merged document with whatever properties your\ndownstream system surfaces (Finder, Explorer, document-management systems,\nemail clients, etc.):\n\n```ts\nawait mergePdfFiles(\n  ['./cover.pdf', './body.pdf'],\n  {\n    metadata: {\n      title: 'Annual Report 2026',\n      author: 'Operations',\n      subject: 'Year-end summary',\n      keywords: ['annual', 'operations', '2026'],\n      creator: 'my-app/1.0',\n      creationDate: new Date(),\n      modificationDate: new Date(),\n    },\n  },\n);\n```\n\nThe `MergePdfUrlsOptions` interface extends the same shape, so all four\nfunctions accept `{ metadata }` the same way. Every field is optional — omit\nwhat you don't want to set.\n\n> The `Producer` field is hard-coded by pdf-lib on save and cannot be\n> customized at this layer.\n\n## Watermark\n\nEvery merge function accepts an optional `watermark` field on the same\n`options` argument. The watermark is stamped on every page of the merged\noutput using Helvetica (no additional fonts are embedded). The feature is\nfully opt-in — when `watermark` is omitted, no extra drawing is performed\nand the original byte-identical fast path is preserved.\n\n```ts\nawait mergePdfFiles(\n  ['./report.pdf'],\n  {\n    watermark: {\n      text: 'CONFIDENTIAL',\n      opacity: 0.18,         // 0–1, default 0.2\n      fontSize: 90,          // points, default 48\n      color: { r: 0.7, g: 0.1, b: 0.1 }, // RGB 0–1, default mid-gray\n      rotate: 45,            // degrees CCW, default 0\n      position: 'center',    // see below\n    },\n  },\n);\n```\n\n`position` accepts a named placement — `'center'` (default), `'top-left'`,\n`'top-right'`, `'bottom-left'`, `'bottom-right'` — or an explicit\n`{ x, y }` in PDF points measured from the bottom-left of the page. Named\nplacements are computed per page so they work on any page size.\n\nInvalid input throws `PdfMergeError`: empty text, opacity outside 0–1,\nnon-positive font size, or color channels outside 0–1.\n\n## Page numbers\n\nStamp continuous page numbering on every page of the merged output.\nCommon use case: stitch several PDFs together and number 1..N across\nthe result. Available on the same `options` argument as everything else.\n\n```ts\nawait mergePdfFiles(\n  ['./cover.pdf', './body.pdf', './appendix.pdf'],\n  {\n    pageNumbers: {\n      format: 'Page {current} of {total}', // tokens: {current}, {total}\n      startAt: 1,                          // first-page value; default 1\n      position: 'bottom-center',           // see below\n      fontSize: 10,                        // default 10\n      color: { r: 0.4, g: 0.4, b: 0.4 },   // default mid-gray\n    },\n  },\n);\n```\n\n`position` accepts a named placement — `'bottom-center'` (default),\n`'top-left'`, `'top-center'`, `'top-right'`, `'bottom-left'`,\n`'bottom-right'` — or an explicit `{ x, y }` in PDF points from the\nbottom-left of the page.\n\nInvalid input throws `PdfMergeError` (empty `format`, non-integer\n`startAt`, non-positive `fontSize`, color channels outside 0–1,\nunknown position).\n\n## Cancellation with `AbortSignal`\n\nEvery merge function accepts `options.signal: AbortSignal`. The merge\naborts cleanly at the next source-iteration boundary, and in-flight\nHTTP fetches in `mergePdfUrls` are cancelled immediately.\n\n```ts\nconst controller = new AbortController();\n\n// Cancel after 30 seconds, or whenever the user clicks \"stop\".\nsetTimeout(() => controller.abort(new Error('took too long')), 30_000);\n\ntry {\n  await mergePdfUrls(urls, { signal: controller.signal, timeoutMs: 60_000 });\n} catch (err) {\n  if (err instanceof Error && err.message === 'took too long') {\n    // user-cancelled — clean up state and move on\n  } else {\n    throw err;\n  }\n}\n```\n\nIf you call `controller.abort(reason)` with a `reason`, that exact value\nis thrown. Without a reason, the merge throws an `AbortError`-shaped\n`DOMException`. Either way, `signal.aborted` checks in caller code\nbehave correctly (this matches the Node `fetch` convention).\n\n## Encrypted source PDFs\n\nSome PDFs declare encryption metadata but contain readable content\nstreams — a quirk of older generators. By default `mergePdfBuffers` and\nthe other merge functions reject these (matching pdf-lib's behavior).\nPass `options.ignoreEncryption: true` to opt in:\n\n```ts\nawait mergePdfBuffers([legacyReport], { ignoreEncryption: true });\n```\n\nThis skips pdf-lib's encryption check at load time. It does **not**\ndecrypt the document — truly password-protected files will still fail\nbecause their content streams cannot be read without the key.\n\n## Page selection\n\nEvery merge function accepts an object form per input that lets you pick which\npages to keep from that document. Pages are **1-indexed** to match what you\nsee in a PDF viewer.\n\n`PageSelector` is either:\n\n- a `number[]` — explicit page numbers, e.g. `[1, 3, 5]`. Duplicates are kept\n  (the page appears multiple times in the output).\n- a `string` — comma-separated ranges, e.g. `\"1-3,5,8-10\"`. Descending ranges\n  (`\"5-1\"`) reverse the page order.\n\nOut-of-range, zero, negative, or unparseable selectors throw `PdfMergeError`.\nInputs without a `pages` field — including all plain-string / plain-Buffer\ninputs — behave exactly as they always have and include every page.\n\n## Errors\n\nAll errors extend `PdfMergeError`, so a single `catch (err: PdfMergeError)`\ncovers everything.\n\n| Error | When it's thrown | Notable fields |\n| --- | --- | --- |\n| `PdfMergeError` | Empty input, invalid input shape, bad URL, disallowed protocol, invalid page selector. | — |\n| `InvalidPdfFormatError` | Input is not valid Base64, not a parseable PDF, or fails the `%PDF-` header check. | — |\n| `PdfFetchError` | HTTP failure, timeout, oversize response, redirect to a disallowed protocol, or non-PDF response body. | `.url` (credentials/query stripped), `.index` (failing input position) |\n\n```ts\nimport { PdfFetchError } from '@abineshsolairaj/pdf-merge';\n\ntry {\n  await mergePdfUrls(urls);\n} catch (err) {\n  if (err instanceof PdfFetchError) {\n    console.error(`URL #${err.index} failed: ${err.url}`);\n  } else {\n    throw err;\n  }\n}\n```\n\n## Security model\n\n`mergePdfUrls` is the higher-risk function — it dereferences caller-supplied\nURLs. Defaults are chosen to be safe out of the box:\n\n- **Protocol allowlist.** Only `http:` and `https:` are accepted; `file:`,\n  `data:`, `ftp:`, etc. are rejected before any socket is opened.\n- **Response size cap.** `maxBytesPerUrl` is enforced both against the\n  declared `Content-Length` and during streaming, so a hostile endpoint that\n  serves an unbounded body cannot exhaust the process heap.\n- **URL sanitization in errors.** Credentials (`user:pass@`) and query\n  strings are stripped from URLs before they appear in error messages or on\n  `PdfFetchError.url`, so signed-URL tokens and HTTP Basic passwords don't\n  leak into logs.\n- **Redirect-protocol check.** If a redirect lands on a disallowed protocol,\n  the request is rejected.\n- **Per-request timeout.** Default `5000 ms` via `AbortController`.\n- **Bounded concurrency.** `concurrency` (default `8`) caps simultaneous\n  in-flight fetches.\n\nWhat this library does **not** do for you:\n\n- **DNS / IP allowlisting.** SSRF to internal hosts via `http://10.0.0.1/…`\n  or cloud metadata endpoints is not blocked — do IP-range filtering at your\n  network or application layer if you accept URLs from end users.\n- **Authentication.** Pass any required tokens via the `headers` option (or\n  per-URL `headers` for signed requests).\n\n## CLI\n\nThe package ships a small `pdf-merge` binary. There are two recommended\nways to invoke it:\n\n### One-shot via `npx` (no install)\n\nAlways pass the full scoped package name, otherwise `npx` will try to\nresolve an unrelated `pdf-merge` package from the registry:\n\n```bash\nnpx @abineshsolairaj/pdf-merge cover.pdf body.pdf appendix.pdf -o out.pdf\n```\n\n### Installed (global or as a dev dependency)\n\nOnce installed, you can call the unscoped `pdf-merge` directly — npm puts\nthe bin on your `PATH`:\n\n```bash\n# global install\nnpm install -g @abineshsolairaj/pdf-merge\npdf-merge cover.pdf body.pdf appendix.pdf -o out.pdf\n\n# or, within a project\nnpm install --save-dev @abineshsolairaj/pdf-merge\nnpx pdf-merge cover.pdf body.pdf appendix.pdf -o out.pdf\n```\n\nMix local files and URLs in one call, and attach a page selector to any input\nwith a trailing colon:\n\n```bash\nnpx @abineshsolairaj/pdf-merge \\\n  cover.pdf \\\n  'body.pdf:2-9' \\\n  https://example.com/appendix.pdf \\\n  -o annual-report.pdf \\\n  --title 'Annual Report 2026' \\\n  --author 'Operations' \\\n  --keywords 'annual,operations,2026'\n```\n\nRun `pdf-merge --help` for the full option list. Metadata flags\n(`--title`, `--author`, `--subject`, `--creator`, `--keywords`) and URL\noptions (`--concurrency`, `--timeout`, `--https-only`) are all supported.\n\n## Module format\n\nThe package ships both ESM and CommonJS builds via a conditional `exports`\nmap, so all of the following work without any tooling tweaks:\n\n```ts\nimport { mergePdfFiles } from '@abineshsolairaj/pdf-merge';     // ESM / bundlers\n```\n\n```js\nconst { mergePdfFiles } = require('@abineshsolairaj/pdf-merge'); // CommonJS\n```\n\nTypeScript definitions are shipped from the CJS build and resolve\nautomatically for both consumers.\n\n## Backward compatibility\n\nPage selection, per-URL headers, and the buffer/file/concurrency options were\nadded as **additive unions** — every previous call signature still\ntype-checks and produces byte-identical output. If you don't pass a `pages`\nfield, the merge runs through the original code path unchanged. A regression\ntest pins this.\n\n## Development\n\n```bash\nnpm install\nnpm run build       # tsc → dist/cjs + dist/esm\nnpm test            # unit + integration tests\nnpm run test:cli    # rebuilds and exercises the CLI binary + dual build\nnpm run test:e2e    # end-to-end harness against real generated PDFs\n```\n\nThe unit suite spins up a local HTTP server and exercises ordering, 404\nhandling, invalid Base64, empty input, timeouts, non-PDF responses, the\nsecurity defaults, page selection, header precedence, and concurrency\ncapping. The e2e harness in `test/e2e.ts` generates real multi-page A4 PDFs\nand exercises every public method end-to-end, including round-tripping each\nmerged output back through `pdf-lib`.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}