Problem
Password handling is split across two divergent implementations with no shared code:
getPdfMetadata uses loadingTask.onPassword — correctly detects encryption, sets an isEncrypted flag, and throws a manually-constructed Error with name = 'PasswordException'. But it uses magic numbers (reason === 1, reason === 2) instead of pdfjs's exported PasswordResponses constants.
parsePdfFile and streamPdfFile bypass onPassword entirely by passing documentInitParameters.password directly to pdfjs. This means: (a) callers get a raw pdfjs PasswordException rather than a library-owned error; (b) there is no way to detect whether a successfully-parsed PDF was encrypted; (c) passing the password via documentInitParameters triggers a latent DataCloneError in some pdfjs versions.
The seam between these two paths is fragile: a bug fix or pdfjs version bump in one place doesn't fix the other, and callers can't write consistent catch logic across the library's functions.
Proposed Interface
Two new error classes (exported from index.ts):
export class PdfPasswordRequiredError extends Error {
readonly name = 'PasswordException'; // preserved for backward compat
readonly code = 'NEED_PASSWORD' as const;
constructor() {
super('No password given');
}
}
export class PdfPasswordIncorrectError extends Error {
readonly name = 'PasswordException'; // preserved for backward compat
readonly code = 'INCORRECT_PASSWORD' as const;
constructor() {
super('Incorrect Password');
}
}
Preserving name = 'PasswordException' and existing message strings means callers catching by err.name or err.message continue to work. The new value is that instanceof PdfPasswordRequiredError and instanceof PdfPasswordIncorrectError now narrow correctly in TypeScript.
One shared internal helper (attachPasswordHandler):
// Internal — not exported
function attachPasswordHandler(
loadingTask: PDFDocumentLoadingTask,
password?: string,
): { getIsEncrypted: () => boolean } {
let isEncrypted = false;
loadingTask.onPassword = (updateCallback, reason) => {
isEncrypted = true;
if (reason === PasswordResponses.NEED_PASSWORD && password) {
updateCallback(password);
} else if (reason === PasswordResponses.INCORRECT_PASSWORD) {
throw new PdfPasswordIncorrectError();
} else {
throw new PdfPasswordRequiredError();
}
};
return { getIsEncrypted: () => isEncrypted };
}
Usage — callers catch typed errors across all functions:
import { pdf2string, getPdfMetadata, PdfPasswordRequiredError, PdfPasswordIncorrectError } from 'afpp';
try {
const pages = await pdf2string('./secure.pdf', { password: 'secret' });
} catch (err) {
if (err instanceof PdfPasswordRequiredError) {
// encrypted, no password supplied
} else if (err instanceof PdfPasswordIncorrectError) {
// wrong password
}
}
// getPdfMetadata continues to return isEncrypted: boolean as before
const meta = await getPdfMetadata('./secure.pdf', { password: 'secret' });
meta.isEncrypted; // true
Dependency Strategy
True external (Mock boundary) — pdfjs is a third-party library with a callback-based password API (onPassword). The strategy is:
- Remove
documentInitParameters.password direct assignment in validateParameters
- Always register
loadingTask.onPassword after getDocument() in all three call sites (parsePdfFile, streamPdfFile, getPdfMetadata)
- Use pdfjs's
PasswordResponses.NEED_PASSWORD and PasswordResponses.INCORRECT_PASSWORD constants instead of numeric literals
- The pdfjs dependency is injected via
getDocument as it is today — no change to how pdfjs is imported or configured
Testing Strategy
New boundary tests to write:
parsePdfFile / pdf2string / pdf2image throw PdfPasswordRequiredError for an encrypted PDF with no password
parsePdfFile / pdf2string / pdf2image throw PdfPasswordIncorrectError for an encrypted PDF with a wrong password
parsePdfFile / pdf2string / pdf2image succeed for an encrypted PDF with the correct password
streamPdfFile variants throw the same typed errors in the same scenarios
getPdfMetadata throws PdfPasswordRequiredError / PdfPasswordIncorrectError (replacing current name: 'PasswordException' assertions)
- All of the above:
err instanceof PdfPasswordRequiredError returns true (verifies instanceof works, not just name comparison)
Old tests to update (not delete):
getPdfMetadata.test.ts: assertions on err.name === 'PasswordException' and message strings remain valid (names/messages are preserved), but add instanceof assertions alongside them
pdf2string.test.ts, pdf2image.test.ts, parsePdf.test.ts, streamPdf.test.ts: add encrypted-PDF test cases that were previously untested (these functions had no onPassword handler)
Test environment: Existing encrypted PDF fixtures are sufficient. No new fixtures needed.
Implementation Recommendations
What the unified password module should own:
- The
onPassword callback registration for every PDFDocumentLoadingTask created in this library
- The mapping from pdfjs's numeric
PasswordResponses to typed library errors
- The
isEncrypted detection flag (via closure, returned as a getter)
What it should hide:
- pdfjs's
PasswordResponses enum values
- The
onPassword callback contract and its updateCallback mechanism
- The difference between "password passed upfront" vs "password passed via callback" (callers use
options.password: string in both cases)
What it should expose:
PdfPasswordRequiredError — thrown when a password-protected PDF is opened without a password
PdfPasswordIncorrectError — thrown when the supplied password is rejected
- Both exported from
index.ts as first-class public API
Migration: No migration needed. password?: string in AfppParseOptions is unchanged. Error name and message strings are preserved. The only observable difference for existing callers is that instanceof PdfPasswordRequiredError and instanceof PdfPasswordIncorrectError now work, which is additive.
Problem
Password handling is split across two divergent implementations with no shared code:
getPdfMetadatausesloadingTask.onPassword— correctly detects encryption, sets anisEncryptedflag, and throws a manually-constructedErrorwithname = 'PasswordException'. But it uses magic numbers (reason === 1,reason === 2) instead of pdfjs's exportedPasswordResponsesconstants.parsePdfFileandstreamPdfFilebypassonPasswordentirely by passingdocumentInitParameters.passworddirectly to pdfjs. This means: (a) callers get a raw pdfjsPasswordExceptionrather than a library-owned error; (b) there is no way to detect whether a successfully-parsed PDF was encrypted; (c) passing the password viadocumentInitParameterstriggers a latentDataCloneErrorin some pdfjs versions.The seam between these two paths is fragile: a bug fix or pdfjs version bump in one place doesn't fix the other, and callers can't write consistent
catchlogic across the library's functions.Proposed Interface
Two new error classes (exported from
index.ts):Preserving
name = 'PasswordException'and existing message strings means callers catching byerr.nameorerr.messagecontinue to work. The new value is thatinstanceof PdfPasswordRequiredErrorandinstanceof PdfPasswordIncorrectErrornow narrow correctly in TypeScript.One shared internal helper (
attachPasswordHandler):Usage — callers catch typed errors across all functions:
Dependency Strategy
True external (Mock boundary) — pdfjs is a third-party library with a callback-based password API (
onPassword). The strategy is:documentInitParameters.passworddirect assignment invalidateParametersloadingTask.onPasswordaftergetDocument()in all three call sites (parsePdfFile,streamPdfFile,getPdfMetadata)PasswordResponses.NEED_PASSWORDandPasswordResponses.INCORRECT_PASSWORDconstants instead of numeric literalsgetDocumentas it is today — no change to how pdfjs is imported or configuredTesting Strategy
New boundary tests to write:
parsePdfFile/pdf2string/pdf2imagethrowPdfPasswordRequiredErrorfor an encrypted PDF with no passwordparsePdfFile/pdf2string/pdf2imagethrowPdfPasswordIncorrectErrorfor an encrypted PDF with a wrong passwordparsePdfFile/pdf2string/pdf2imagesucceed for an encrypted PDF with the correct passwordstreamPdfFilevariants throw the same typed errors in the same scenariosgetPdfMetadatathrowsPdfPasswordRequiredError/PdfPasswordIncorrectError(replacing currentname: 'PasswordException'assertions)err instanceof PdfPasswordRequiredErrorreturnstrue(verifiesinstanceofworks, not just name comparison)Old tests to update (not delete):
getPdfMetadata.test.ts: assertions onerr.name === 'PasswordException'and message strings remain valid (names/messages are preserved), but addinstanceofassertions alongside thempdf2string.test.ts,pdf2image.test.ts,parsePdf.test.ts,streamPdf.test.ts: add encrypted-PDF test cases that were previously untested (these functions had noonPasswordhandler)Test environment: Existing encrypted PDF fixtures are sufficient. No new fixtures needed.
Implementation Recommendations
What the unified password module should own:
onPasswordcallback registration for everyPDFDocumentLoadingTaskcreated in this libraryPasswordResponsesto typed library errorsisEncrypteddetection flag (via closure, returned as a getter)What it should hide:
PasswordResponsesenum valuesonPasswordcallback contract and itsupdateCallbackmechanismoptions.password: stringin both cases)What it should expose:
PdfPasswordRequiredError— thrown when a password-protected PDF is opened without a passwordPdfPasswordIncorrectError— thrown when the supplied password is rejectedindex.tsas first-class public APIMigration: No migration needed.
password?: stringinAfppParseOptionsis unchanged. Errornameandmessagestrings are preserved. The only observable difference for existing callers is thatinstanceof PdfPasswordRequiredErrorandinstanceof PdfPasswordIncorrectErrornow work, which is additive.