From 8b98befce8f3593a9e3382b074b1528578129ac2 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 12 Jun 2026 12:04:01 +1000 Subject: [PATCH 1/3] feat: add standalone timestamp and validation data APIs Add PDF.addTimestamp(), addValidationData(), and addArchivalData() for multi-signer PAdES flows: upgrade existing B-T signatures to B-LT/B-LTA after the fact instead of only at signing time. Refactor signature field lookup into prepareSignatureField() with support for reusing pre-allocated fields, and switch field errors to typed SignatureError codes. --- .gitignore | 4 + content/docs/guides/signatures/index.mdx | 73 +++ src/api/pdf-page.ts | 2 +- src/api/pdf-signature.ts | 615 +++++++++++++++--- src/api/pdf.ts | 140 +++- src/document/forms/acro-form.ts | 2 - src/index.ts | 6 + .../signatures/lta-finalization.test.ts | 312 +++++++++ src/integration/signatures/signing.test.ts | 29 +- src/integration/signatures/test-helpers.ts | 34 + .../signatures/timestamping.test.ts | 302 +++++++++ src/signatures/index.ts | 6 + src/signatures/ltv/dss-builder.ts | 7 +- src/signatures/ltv/gatherer.ts | 1 - src/signatures/types.ts | 168 +++++ 15 files changed, 1565 insertions(+), 136 deletions(-) create mode 100644 src/integration/signatures/lta-finalization.test.ts create mode 100644 src/integration/signatures/test-helpers.ts create mode 100644 src/integration/signatures/timestamping.test.ts diff --git a/.gitignore b/.gitignore index fd26d33..44adb34 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ reports/bench-results.json # Temporary files tmp/ + +# opencode tooling lockfiles (machine-local) +.opencode/package-lock.json +.opencode/node_modules/ diff --git a/content/docs/guides/signatures/index.mdx b/content/docs/guides/signatures/index.mdx index 7b2087a..895e0ac 100644 --- a/content/docs/guides/signatures/index.mdx +++ b/content/docs/guides/signatures/index.mdx @@ -209,6 +209,79 @@ const fullySigned = await pdf2.sign({ signer: signer2 }); await writeFile("signed.pdf", fullySigned.bytes); ``` +## Finalizing Multi-Signer Documents + +In multi-signer workflows, each recipient signs at B-T level (fast, no +revocation lookups per signer). Once everyone has signed, finalize the +document in one pass. + +### Add Validation Data (B-T → B-LT) + +`addValidationData()` gathers certificates, OCSP responses, and CRLs for +every signed signature field and writes them as a single DSS incremental +update: + +```ts +const { bytes, warnings, signatureCount } = await pdf.addValidationData(); + +console.log(`Embedded LTV for ${signatureCount} signatures`); +``` + +Validation data is fetched once per issuer (signers sharing a CA don't +trigger duplicate lookups) and merged with any existing DSS. + +### Add a Document Timestamp + +`addTimestamp()` appends a `/DocTimeStamp` signature whose ByteRange covers +the entire document, sealing all prior signatures with a TSA-attested time: + +```ts +const tsa = new HttpTimestampAuthority("http://timestamp.digicert.com"); + +const { bytes } = await pdf.addTimestamp({ + timestampAuthority: tsa, + longTermValidation: true, // embed LTV for the timestamp's own chain +}); +``` + +Pass `fieldName` to fill a pre-allocated empty signature field — useful when +the form structure must be locked down before a certification signature: + +```ts +await pdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "ArchivalTimestamp", // reserved earlier via createSignatureField() +}); +``` + +### Full B-LTA Finalization + +`addArchivalData()` combines both steps: it gathers validation data for all +existing signatures, adds an archival document timestamp, and embeds the +timestamp's own validation data: + +```ts +const tsa = new HttpTimestampAuthority("http://timestamp.digicert.com"); + +const { bytes, warnings, signatureCount } = await pdf.addArchivalData({ + timestampAuthority: tsa, +}); + +await writeFile("sealed.pdf", bytes); +``` + +These methods refuse to run when the document cannot be saved incrementally +(for example, linearized documents or documents recovered from corruption) +and throw a `SignatureError` instead — a full rewrite would invalidate every +existing signature. + + + If `addTimestamp()`, `addValidationData()`, or `addArchivalData()` throws after partial progress + (for example, the timestamp authority is unreachable after a DSS update was already written), the + in-memory `PDF` instance may be out of sync with its bytes. Don't keep using it: discard the + instance and reload from the last known-good bytes with `PDF.load()`. + + ## Check Existing Signatures ```ts diff --git a/src/api/pdf-page.ts b/src/api/pdf-page.ts index dadec4f..628c15c 100644 --- a/src/api/pdf-page.ts +++ b/src/api/pdf-page.ts @@ -373,8 +373,8 @@ export class PDFPage { if (rotate) { const value = rotate.value % 360; - // Normalize to 0, 90, 180, 270 + // Normalize to 0, 90, 180, 270 if (value === 90 || value === -270) { return 90; } diff --git a/src/api/pdf-signature.ts b/src/api/pdf-signature.ts index 9968013..cdf5a7a 100644 --- a/src/api/pdf-signature.ts +++ b/src/api/pdf-signature.ts @@ -18,7 +18,7 @@ import { PdfArray } from "#src/objects/pdf-array"; import { PdfDict } from "#src/objects/pdf-dict"; import { PdfName } from "#src/objects/pdf-name"; import { PdfNumber } from "#src/objects/pdf-number"; -import type { PdfRef } from "#src/objects/pdf-ref"; +import { PdfRef } from "#src/objects/pdf-ref"; import { PdfString } from "#src/objects/pdf-string"; import { CAdESDetachedBuilder } from "#src/signatures/formats/cades-detached"; import { PKCS7DetachedBuilder } from "#src/signatures/formats/pkcs7-detached"; @@ -36,6 +36,8 @@ import { } from "#src/signatures/placeholder"; import { DefaultRevocationProvider } from "#src/signatures/revocation"; import { + type ArchivalDataOptions, + type ArchivalDataResult, type DigestAlgorithm, type PAdESLevel, type RevocationProvider, @@ -45,6 +47,10 @@ import { type SignWarning, type SubFilter, type TimestampAuthority, + type TimestampOptions, + type TimestampResult, + type ValidationDataOptions, + type ValidationDataResult, } from "#src/signatures/types"; import { escapePdfString, hashData } from "#src/signatures/utils"; @@ -133,7 +139,7 @@ export class PDFSignature { const firstPageRef = this.pdf.context.pages.getPage(0); if (!firstPageRef) { - throw new Error("Document has no pages - cannot create signature field"); + throw new SignatureError("NO_PAGES", "Document has no pages - cannot create signature field"); } // Create signature dictionary with placeholders @@ -164,10 +170,12 @@ export class PDFSignature { const signatureRef = this.pdf.context.registry.register(signatureDict); // Find or create signature field - this.findOrCreateSignatureField({ + this.prepareSignatureField({ fieldName: resolved.fieldName, pageRef: firstPageRef, - signatureRef, + valueRef: signatureRef, + namePrefix: "Signature_", + reuseFirstEmpty: true, }); // Save incrementally to get bytes with placeholders @@ -248,24 +256,23 @@ export class PDFSignature { // For B-LTA, add document timestamp after DSS, then add DSS for the timestamp if (resolved.archivalTimestamp && resolved.timestampAuthority) { - const docTsToken = await this.addDocumentTimestamp( - resolved.timestampAuthority, - resolved.digestAlgorithm, - ); + const paddedTimestampBytes = await this.placeDocumentTimestamp({ + timestampAuthority: resolved.timestampAuthority, + digestAlgorithm: resolved.digestAlgorithm, + estimatedSize: DEFAULT_PLACEHOLDER_SIZE, + }); // Add DSS for the document timestamp's certificate chain. // This is more proactive than EU DSS (which waits for future LTA extensions), // but ensures the timestamp is fully LTV-enabled from the start. - if (docTsToken) { - const docTsLtvData = await this.gatherTimestampLtvData( - docTsToken, - resolved.revocationProvider, - warnings, - ); + const docTsLtvData = await this.gatherTimestampLtvData( + paddedTimestampBytes, + resolved.revocationProvider, + warnings, + ); - if (docTsLtvData) { - await this.addDss(docTsLtvData); - } + if (docTsLtvData) { + await this.addDss(docTsLtvData); } } } @@ -280,58 +287,82 @@ export class PDFSignature { } /** - * Find or create a signature field. + * Find or create the /FT /Sig field that will hold a signature or document + * timestamp value, then convert it to the merged field+widget model + * (the invisible widget pattern used for all signatures in this library). + * + * Lookup behavior: + * - `fieldName` provided + matches an unsigned signature field -> reuse + * - `fieldName` provided + matches a signed signature field -> throw + * - `fieldName` provided + matches a non-signature field -> throw + * - `fieldName` provided + no match -> create + * - `fieldName` omitted + `reuseFirstEmpty` -> reuse first + * empty signature field, or create with `N` + * - `fieldName` omitted otherwise -> create with + * `N` */ - private findOrCreateSignatureField(options: { + private prepareSignatureField(options: { fieldName?: string; pageRef: PdfRef; - signatureRef: PdfRef; + valueRef: PdfRef; + namePrefix: string; + reuseFirstEmpty: boolean; }): void { - const { fieldName, pageRef, signatureRef } = options; + const { fieldName, pageRef, valueRef, namePrefix, reuseFirstEmpty } = options; const form = this.pdf.getOrCreateForm(); + // Collect existing field names so we can both look up a named field + // and generate a unique fallback name when none is supplied. const existingNames = new Set(); let fieldDict: PdfDict | undefined; - const fields = form.getFields(); - - for (const field of fields) { + for (const field of form.getFields()) { existingNames.add(field.name); // If requested name matches an existing field if (fieldName && field.name === fieldName) { - if (field instanceof SignatureField) { - if (field.isSigned()) { - throw new Error(`Signature field "${fieldName}" is already signed`); - } + if (!(field instanceof SignatureField)) { + throw new SignatureError( + "FIELD_NOT_SIGNATURE", + `Field "${fieldName}" exists but is not a signature field`, + ); + } - fieldDict = field.getDict(); // Use existing unsigned field - break; + if (field.isSigned()) { + throw new SignatureError( + "FIELD_ALREADY_SIGNED", + `Signature field "${fieldName}" is already signed`, + ); } - throw new Error(`Field "${fieldName}" exists but is not a signature field`); + fieldDict = field.getDict(); // Use existing unsigned field + break; } - // If no name requested, look for first empty signature field - if (!fieldName && field instanceof SignatureField && !field.isSigned()) { + // If no name requested, optionally reuse the first empty signature field + if (!fieldName && reuseFirstEmpty && field instanceof SignatureField && !field.isSigned()) { fieldDict = field.getDict(); break; } } if (!fieldDict) { + // PDFForm handles registry registration, /Fields, and /SigFlags 3. fieldDict = form - .createSignatureField(fieldName ?? generateUniqueName(existingNames, "Signature_")) + .createSignatureField(fieldName ?? generateUniqueName(existingNames, namePrefix)) .getDict(); } // Set signature value - fieldDict.set("V", signatureRef); + fieldDict.set("V", valueRef); - // Convert to merged field+widget model (common for invisible signatures) - // Remove /Kids if present (we're merging into a single object) + // Convert to merged field+widget model (common for invisible signatures). + // If the field carried widget kids (e.g. pre-allocated by an external + // tool), detach them from their pages first so no dangling /Annots + // references remain after we drop /Kids. + this.removeWidgetKidsFromPages(fieldDict); fieldDict.delete("Kids"); // Add widget annotation properties @@ -345,6 +376,52 @@ export class PDFSignature { ); } + /** + * Remove a field's widget kids from every page's /Annots array. + * + * Merging a field with widget kids into a single field+widget object would + * otherwise leave those widgets referenced from page /Annots while no + * longer being listed in the field's /Kids - an inconsistent structure + * that confuses viewers. + */ + private removeWidgetKidsFromPages(fieldDict: PdfDict): void { + const registry = this.pdf.context.registry; + const resolve = registry.resolve.bind(registry); + const kids = fieldDict.getArray("Kids", resolve); + + if (!kids || kids.length === 0) { + return; + } + + const kidKeys = new Set(); + + for (const kid of kids) { + if (kid instanceof PdfRef) { + kidKeys.add(`${kid.objectNumber} ${kid.generation}`); + } + } + + if (kidKeys.size === 0) { + return; + } + + for (const page of this.pdf.getPages()) { + const annots = page.dict.getArray("Annots", resolve); + + if (!annots) { + continue; + } + + for (let i = annots.length - 1; i >= 0; i--) { + const item = annots.at(i); + + if (item instanceof PdfRef && kidKeys.has(`${item.objectNumber} ${item.generation}`)) { + annots.remove(i); + } + } + } + } + /** * Check for MDP (certification signature) violations. */ @@ -389,14 +466,8 @@ export class PDFSignature { */ async addDss(ltvData: LtvData): Promise { const registry = this.pdf.context.registry; - - // Get catalog const catalogDict = this.pdf.getCatalog(); - if (!catalogDict) { - throw new Error("Document has no catalog"); - } - // Load existing DSS for merging, or create new builder const dssBuilder = await DSSBuilder.fromCatalog(catalogDict, registry); @@ -407,38 +478,372 @@ export class PDFSignature { const dssRef = dssBuilder.build(); catalogDict.set("DSS", dssRef); - // Save and reload - const savedBytes = await this.pdf.save({ incremental: true }); - await this.pdf.reload(savedBytes); + await this.saveAndReload(); } /** - * Add a document timestamp for archival (B-LTA). + * Save incrementally and reload the PDF instance so it reflects the saved + * bytes. Skips the reload (a full re-parse) when nothing was written - + * `save()` short-circuits and returns the current bytes in that case. + */ + private async saveAndReload(): Promise { + const hadChanges = this.pdf.hasChanges(); + const bytes = await this.pdf.save({ incremental: true }); + + if (hadChanges) { + await this.pdf.reload(bytes); + } + + return bytes; + } + + /** + * Throw when the document cannot be saved incrementally. + * + * Timestamping and validation-data updates exist to extend documents that + * already carry signatures. A silent fall back to a full rewrite would + * change every byte offset and invalidate all existing signatures, so we + * refuse up front instead. + */ + private ensureIncrementalSave(operation: string): void { + const blocker = this.pdf.canSaveIncrementally(); + + if (blocker) { + throw new SignatureError( + "INCREMENTAL_SAVE_BLOCKED", + `${operation} requires an incremental save to preserve existing signatures, ` + + `but incremental save is not possible (${blocker}). ` + + `Save the document with a full rewrite first, reload it, and retry.`, + ); + } + } + + /** + * Add an archival document timestamp to the PDF. + * + * Creates a `/Type /DocTimeStamp` signature whose ByteRange covers the + * entire current document, extending the validity of any prior signatures. + * This is the timestamping step used at the end of a PAdES B-LTA flow + * when signatures have been appended. * - * Creates a document timestamp signature that covers the entire document - * including previous signatures and DSS data. + * Does **not** gather validation data for pre-existing signatures - use + * `addValidationData()` for that, or `addArchivalData()` to do both in + * one call. * - * After adding the timestamp, the PDF is reloaded with the updated bytes. + * The PDF instance is reloaded with the updated bytes, so subsequent + * calls (e.g. another `addTimestamp()`) operate on the timestamped state. * - * @param timestampAuthority The timestamp authority to use - * @param digestAlgorithm Digest algorithm (defaults to SHA-256) - * @returns The timestamp token bytes (for gathering LTV data) + * If this method throws after partial progress (e.g. the TSA request + * fails), the in-memory PDF instance may be out of sync with its bytes. + * Discard the instance and reload from the last known-good bytes. + * + * @param options Timestamping options including the TSA + * @returns The PDF bytes with the timestamp embedded, plus any warnings + * + * @example + * ```typescript + * // Append an archival timestamp to an already-signed PDF. + * const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + * const { bytes } = await pdf.addTimestamp({ + * timestampAuthority: tsa, + * longTermValidation: true, + * }); + * ``` */ - async addDocumentTimestamp( - timestampAuthority: TimestampAuthority, - digestAlgorithm: DigestAlgorithm = "SHA-256", - ): Promise { - const estimatedSize = DEFAULT_PLACEHOLDER_SIZE; - const registry = this.pdf.context.registry; + async addTimestamp(options: TimestampOptions): Promise { + if (!options.timestampAuthority) { + throw new SignatureError("INVALID_OPTIONS", "addTimestamp() requires a timestampAuthority"); + } + + this.ensureIncrementalSave("addTimestamp()"); + + return this.addTimestampInternal(options); + } + + /** + * Implementation of `addTimestamp()`, with an optional shared gatherer so + * `addArchivalData()` can reuse OCSP/CRL/AIA results fetched while + * gathering validation data for existing signatures. + */ + private async addTimestampInternal( + options: TimestampOptions, + gatherer?: LtvDataGatherer, + ): Promise { + const warnings: SignWarning[] = []; + const digestAlgorithm = options.digestAlgorithm ?? "SHA-256"; + const estimatedSize = options.estimatedSize ?? DEFAULT_PLACEHOLDER_SIZE; + const longTermValidation = options.longTermValidation ?? false; + + // Place the document timestamp (writes /DocTimeStamp dict, registers the + // field with AcroForm, saves incrementally, requests the TSA token, and + // patches the placeholders). Returns the padded /Contents bytes that + // viewers use as the VRI key for the next DSS update. + const paddedTimestampBytes = await this.placeDocumentTimestamp({ + timestampAuthority: options.timestampAuthority, + digestAlgorithm, + estimatedSize, + fieldName: options.fieldName, + }); + + // Optionally embed LTV data for the timestamp's certificate chain so the + // timestamp itself remains verifiable after the TSA certificate expires. + if (longTermValidation) { + const ltvData = await this.gatherTimestampLtvData( + paddedTimestampBytes, + options.revocationProvider, + warnings, + gatherer, + ); + + if (ltvData) { + await this.addDss(ltvData); + } + } + + const bytes = await this.saveAndReload(); + + return { bytes, warnings }; + } + + /** + * Gather LTV (Long-Term Validation) data for every signed signature + * field currently in the document and write it as a single DSS + * incremental update. + * + * This upgrades the validation grade of every existing signature in the + * document — turning B-T signatures into B-LT and ensuring document + * timestamps have their TSA chain embedded for offline validation. + * Validation data is fetched once per issuer (shared OCSP/CRL cache) + * and merged with any existing DSS, deduplicating certs/OCSP/CRL. + * + * Does **not** add a timestamp - use `addTimestamp()` for that, or + * `addArchivalData()` to do both in one call. + * + * Safe to call on a document with no signatures (returns + * `signatureCount: 0`, no DSS update written). + * + * If this method throws after partial progress, the in-memory PDF + * instance may be out of sync with its bytes. Discard the instance and + * reload from the last known-good bytes. + * + * @example + * ```typescript + * // After every recipient has signed (B-T), upgrade all sigs to B-LT. + * await pdf.addValidationData(); + * ``` + */ + async addValidationData(options: ValidationDataOptions = {}): Promise { + this.ensureIncrementalSave("addValidationData()"); + + // A single LtvDataGatherer so its OCSP / CRL cache is shared across + // every signature we process. + const gatherer = new LtvDataGatherer({ + revocationProvider: options.revocationProvider ?? new DefaultRevocationProvider(), + }); + + return this.addValidationDataInternal(gatherer); + } + + /** + * Implementation of `addValidationData()`, with the gatherer injected so + * `addArchivalData()` can share one OCSP/CRL cache across both its + * validation-data and timestamp steps. + */ + private async addValidationDataInternal( + gatherer: LtvDataGatherer, + ): Promise { + const warnings: SignWarning[] = []; + + // Collect signed signature fields - both regular signatures and + // /Type /DocTimeStamp use a /FT /Sig field with a /V signature dict, + // so SignatureField + isSigned() finds both. Without an AcroForm there + // are no signature fields at all. + const form = this.pdf.getForm(); + const signedFields = form?.getSignatureFields().filter(field => field.isSigned()) ?? []; + + if (signedFields.length === 0) { + const bytes = await this.saveAndReload(); + + return { bytes, warnings, signatureCount: 0 }; + } + + // Build a single DSSBuilder that merges with whatever DSS already + // exists in the catalog. + const catalogDict = this.pdf.getCatalog(); + const builder = await DSSBuilder.fromCatalog(catalogDict, this.pdf.context.registry); + + let processed = 0; + + for (const field of signedFields) { + const sigDict = field.getSignatureDict(); + + if (!sigDict) { + warnings.push({ + code: "LTV_GATHER_FAILED", + message: `Signature field "${field.name}" has no /V dictionary`, + }); + continue; + } + + // The padded /Contents bytes are exactly what viewers SHA-1 to + // compute the VRI key, so we must pass the raw bytes including + // zero padding (PdfString.bytes preserves that). + const contents = sigDict.get("Contents"); + + if (!(contents instanceof PdfString)) { + warnings.push({ + code: "LTV_GATHER_FAILED", + message: `Signature field "${field.name}" has no /Contents string`, + }); + continue; + } + + try { + const ltvData = await gatherer.gather(contents.bytes); + + // Prefix gatherer warnings with the field name so callers can + // tell which signature each warning is about. + for (const w of ltvData.warnings) { + warnings.push({ + code: w.code, + message: `${field.name}: ${w.message}`, + }); + } + + await builder.addLtvData(ltvData); + processed += 1; + } catch (error) { + warnings.push({ + code: "LTV_GATHER_FAILED", + message: `Could not gather LTV for "${field.name}": ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + + // If nothing could be gathered, don't write an empty DSS revision. + if (processed === 0) { + const bytes = await this.saveAndReload(); + + return { bytes, warnings, signatureCount: 0 }; + } + + // Write a single incremental update for the DSS, even if some + // signatures failed - partial data is still useful for verifiers. + const dssRef = builder.build(); + + catalogDict.set("DSS", dssRef); + + const bytes = await this.saveAndReload(); + + return { bytes, warnings, signatureCount: processed }; + } + + /** + * Finalize the document with full PAdES B-LTA semantics in a single + * call: gather LTV for every existing signature, write a DSS update, + * add an archival `/DocTimeStamp`, then add a second DSS update for + * the timestamp's own certificate chain. + * + * Equivalent to: + * + * ```typescript + * await pdf.addValidationData({ revocationProvider }); + * await pdf.addTimestamp({ + * timestampAuthority, + * longTermValidation: true, + * revocationProvider, + * ... + * }); + * ``` + * + * Use this as the last step of a multi-signer flow once every signer + * has appended their signature and you want to seal the document. + * + * If this method throws after partial progress (e.g. the TSA request + * fails after the DSS update was written), the in-memory PDF instance + * may be out of sync with its bytes. Discard the instance and reload + * from the last known-good bytes. + * + * @example + * ```typescript + * const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + * const { bytes, warnings } = await pdf.addArchivalData({ + * timestampAuthority: tsa, + * }); + * ``` + */ + async addArchivalData(options: ArchivalDataOptions): Promise { + if (!options.timestampAuthority) { + throw new SignatureError( + "INVALID_OPTIONS", + "addArchivalData() requires a timestampAuthority", + ); + } + + this.ensureIncrementalSave("addArchivalData()"); + + const warnings: SignWarning[] = []; + + // One gatherer for both steps so OCSP/CRL responses fetched for the + // existing signatures (typically including the same TSA chain the + // archival timestamp will use) are not re-fetched in step 2. + const gatherer = new LtvDataGatherer({ + revocationProvider: options.revocationProvider ?? new DefaultRevocationProvider(), + }); + + // Step 1: gather LTV for all existing signatures and write one DSS. + const validation = await this.addValidationDataInternal(gatherer); + + warnings.push(...validation.warnings); + + // Step 2: add the archival timestamp and let addTimestamp handle the + // timestamp's own LTV / second DSS write. + const timestamp = await this.addTimestampInternal( + { + timestampAuthority: options.timestampAuthority, + digestAlgorithm: options.digestAlgorithm, + estimatedSize: options.estimatedSize, + fieldName: options.fieldName, + longTermValidation: true, + revocationProvider: options.revocationProvider, + }, + gatherer, + ); + + warnings.push(...timestamp.warnings); + + return { + bytes: timestamp.bytes, + warnings, + signatureCount: validation.signatureCount, + }; + } + + /** + * Place a document timestamp in the PDF (shared by `addTimestamp()` and the + * B-LTA path of `sign()`). + * + * Returns the padded timestamp bytes (raw token + zero padding to fill the + * placeholder) so callers can compute the SHA-1 VRI key per ETSI EN 319 + * 142-2 / PDF 2.0 § 12.8.4.3. + */ + private async placeDocumentTimestamp(options: { + timestampAuthority: TimestampAuthority; + digestAlgorithm: DigestAlgorithm; + estimatedSize: number; + fieldName?: string; + }): Promise { + const { timestampAuthority, digestAlgorithm, estimatedSize, fieldName } = options; - // Get first page for widget const firstPageRef = this.pdf.context.pages.getPage(0); if (!firstPageRef) { - throw new Error("Document has no pages"); + throw new SignatureError("NO_PAGES", "Document has no pages - cannot create timestamp field"); } - // Create document timestamp dictionary with placeholders + // Build the /Type /DocTimeStamp dictionary with placeholders. const timestampDict = PdfDict.of({ Type: PdfName.of("DocTimeStamp"), Filter: PdfName.of("Adobe.PPKLite"), @@ -447,50 +852,51 @@ export class PDFSignature { Contents: createContentsPlaceholderObject(estimatedSize), }); - const timestampRef = registry.register(timestampDict); - - // Create signature field for timestamp - const fieldName = `DocTimeStamp_${Date.now()}`; - const fieldDict = PdfDict.of({ - Type: PdfName.of("Annot"), - Subtype: PdfName.of("Widget"), - FT: PdfName.of("Sig"), - T: PdfString.fromString(fieldName), - V: timestampRef, - F: PdfNumber.of(132), - P: firstPageRef, - Rect: new PdfArray([PdfNumber.of(0), PdfNumber.of(0), PdfNumber.of(0), PdfNumber.of(0)]), + const timestampRef = this.pdf.context.registry.register(timestampDict); + + // Create a /FT /Sig field for the timestamp and register it with the + // AcroForm so /SigFlags is set and the field is reachable from /Fields. + // + // Reusing a pre-allocated field (via fieldName) is the recommended + // pattern for multi-signer AdES / DocMDP flows where the author locks + // down the /AcroForm /Fields structure before the certification + // signature is applied. Unlike signing, we never auto-reuse the first + // empty signature field when no name is given - users typically reserve + // those for actual signers, not timestamps. + this.prepareSignatureField({ + fieldName, + pageRef: firstPageRef, + valueRef: timestampRef, + namePrefix: "Timestamp_", + reuseFirstEmpty: false, }); - registry.register(fieldDict); - - // Save to get bytes with placeholders - const savedBytes = await this.pdf.save({ incremental: true }); + // Save incrementally so the file contains the new dict with placeholders. + const pdfBytes = await this.pdf.save({ incremental: true }); - // Find placeholders and calculate ByteRange - const placeholders = findPlaceholders(savedBytes); - const byteRange = calculateByteRange(savedBytes, placeholders); + // Locate the placeholders, compute the ByteRange, and patch it in place. + const placeholders = findPlaceholders(pdfBytes); + const byteRange = calculateByteRange(pdfBytes, placeholders); - // Patch ByteRange - patchByteRange(savedBytes, placeholders, byteRange); + patchByteRange(pdfBytes, placeholders, byteRange); - // Hash and get timestamp - const signedBytes = extractSignedBytes(savedBytes, byteRange); + // Hash everything outside the /Contents placeholder and request a token. + const signedBytes = extractSignedBytes(pdfBytes, byteRange); const documentHash = await hashData(signedBytes, digestAlgorithm); const timestampToken = await timestampAuthority.timestamp(documentHash, digestAlgorithm); - // Patch Contents - patchContents(savedBytes, placeholders, timestampToken); + // Write the token into the /Contents placeholder. + patchContents(pdfBytes, placeholders, timestampToken); - // Reload - await this.pdf.reload(savedBytes); + // Reload so the PDF instance reflects the on-disk state. + await this.pdf.reload(pdfBytes); - // Return padded timestamp bytes for correct VRI hash computation. - // The VRI key is the SHA-1 hash of the FULL /Contents value as stored - // in the PDF, including zero padding - not just the raw timestamp token. - const contentsSize = placeholders.contentsLength / 2; // Hex chars -> bytes + // Return the padded /Contents bytes (raw token + trailing zeros) for VRI + // hash computation. The VRI key is SHA-1 over the full /Contents value + // as stored, including the zero padding - not just the raw token. + const contentsSize = placeholders.contentsLength / 2; // hex chars -> bytes const paddedTimestampBytes = new Uint8Array(contentsSize); - paddedTimestampBytes.set(timestampToken); // Remaining bytes are zeros + paddedTimestampBytes.set(timestampToken); return paddedTimestampBytes; } @@ -499,17 +905,25 @@ export class PDFSignature { * Gather LTV data for a timestamp token. * * Used for B-LTA to add validation data for the document timestamp. + * + * When a shared gatherer is provided (by `addArchivalData()`), it is + * reused so cached OCSP/CRL responses carry over. Timestamp tokens carry + * no embedded signature timestamps, so the shared gatherer's + * `gatherTimestampLtv: true` default has no effect here. */ private async gatherTimestampLtvData( timestampToken: Uint8Array, revocationProvider: RevocationProvider | undefined, warnings: SignWarning[], + sharedGatherer?: LtvDataGatherer, ): Promise { // Use LtvDataGatherer - timestamp tokens are just CMS structures - const gatherer = new LtvDataGatherer({ - revocationProvider: revocationProvider ?? new DefaultRevocationProvider(), - gatherTimestampLtv: false, // Don't recurse for doc timestamps - }); + const gatherer = + sharedGatherer ?? + new LtvDataGatherer({ + revocationProvider: revocationProvider ?? new DefaultRevocationProvider(), + gatherTimestampLtv: false, // Don't recurse for doc timestamps + }); try { const ltvData = await gatherer.gather(timestampToken); @@ -525,6 +939,7 @@ export class PDFSignature { code: "DOC_TS_NO_CERTS", message: "No certificates found in document timestamp", }); + return null; } @@ -534,6 +949,7 @@ export class PDFSignature { code: "DOC_TS_LTV_FAILED", message: `Could not gather LTV data for document timestamp: ${error instanceof Error ? error.message : String(error)}`, }); + return null; } } @@ -560,7 +976,6 @@ export class PDFSignature { } // Validate timestamp requirements - if ( (options.level === "B-T" || options.level === "B-LT" || options.level === "B-LTA") && !options.timestampAuthority diff --git a/src/api/pdf.ts b/src/api/pdf.ts index e2cbbea..2de5886 100644 --- a/src/api/pdf.ts +++ b/src/api/pdf.ts @@ -55,7 +55,16 @@ import { generateEncryption, reconstructEncryptDict } from "#src/security/encryp import { PermissionDeniedError } from "#src/security/errors"; import { DEFAULT_PERMISSIONS, type Permissions } from "#src/security/permissions"; import type { StandardSecurityHandler } from "#src/security/standard-handler.ts"; -import type { SignOptions, SignResult } from "#src/signatures/types"; +import type { + ArchivalDataOptions, + ArchivalDataResult, + SignOptions, + SignResult, + TimestampOptions, + TimestampResult, + ValidationDataOptions, + ValidationDataResult, +} from "#src/signatures/types"; import type { FindTextOptions, PageText, TextMatch } from "#src/text/types"; import { writeComplete, writeIncremental } from "#src/writer/pdf-writer"; import { randomBytes } from "@noble/ciphers/utils.js"; @@ -2763,6 +2772,133 @@ export class PDF { return signature.sign(options); } + /** + * Add an archival document timestamp to the PDF. + * + * Creates a `/Type /DocTimeStamp` signature whose ByteRange covers the + * entire current document, sealing it with a trusted RFC 3161 timestamp. + * + * This is the timestamping step in a PAdES B-LTA flow: after one or more + * signatures have been appended (each as an incremental update), call + * `addTimestamp()` to lock the document state with a TSA-attested time. + * The timestamp extends the validity of all prior signatures because its + * ByteRange covers them. + * + * Does **not** gather validation data for pre-existing signatures - use + * {@link addValidationData} for that, or {@link addArchivalData} to do + * both in one call. + * + * After timestamping, the PDF instance is automatically reloaded with the + * updated bytes, so you can call `addTimestamp()` again or `save()` to get + * the final bytes. If this method throws after partial progress (e.g. the + * TSA request fails), the in-memory instance may be out of sync with its + * bytes - discard it and reload from the last known-good bytes. + * + * @param options Timestamping options including the TSA + * @returns The PDF bytes with the timestamp embedded, plus any warnings + * + * @throws {SignatureError} If `timestampAuthority` is missing, if the + * document cannot be saved incrementally (which would invalidate + * existing signatures), or if the document has no pages + * @throws {PlaceholderError} If the reserved size is too small for the token + * + * @example + * ```typescript + * import { HttpTimestampAuthority } from "@libpdf/core"; + * + * // Sign first (B-T or B-LT), then seal with an archival timestamp. + * const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + * await pdf.sign({ signer, level: "B-LT", timestampAuthority: tsa }); + * const { bytes } = await pdf.addTimestamp({ + * timestampAuthority: tsa, + * longTermValidation: true, + * }); + * ``` + */ + async addTimestamp(options: TimestampOptions): Promise { + const signature = new PDFSignature(this); + + return signature.addTimestamp(options); + } + + /** + * Gather LTV (Long-Term Validation) data for every signed signature + * field in the document and write it as a single DSS incremental + * update. + * + * Upgrades B-T signatures to B-LT in one shot. Use this after a + * multi-signer flow where each recipient signed at B-T level and you + * now want full long-term validation data embedded for all of them. + * + * Reuses one OCSP/CRL cache across signatures so issuers shared + * between signers don't get re-fetched. Existing DSS contents are + * merged with the new data (certs / OCSP / CRL are deduplicated by + * SHA-1). + * + * Does **not** add a timestamp - use {@link addTimestamp} for that, or + * {@link addArchivalData} to do both in one call. + * + * After this call the PDF instance is reloaded with the updated bytes, + * so subsequent operations (e.g. `addTimestamp()`) see the new DSS. + * + * @param options Optional revocation provider override + * @returns Bytes, warnings, and the number of signatures processed + * + * @throws {SignatureError} If the document cannot be saved incrementally + * (which would invalidate existing signatures) + * + * @example + * ```typescript + * const { signatureCount, warnings } = await pdf.addValidationData(); + * console.log(`Embedded LTV for ${signatureCount} signatures`); + * ``` + */ + async addValidationData(options: ValidationDataOptions = {}): Promise { + const signature = new PDFSignature(this); + + return signature.addValidationData(options); + } + + /** + * Finalize the document with full PAdES B-LTA: gather LTV for every + * existing signature, embed a DSS, add an archival document timestamp, + * and embed a second DSS for the timestamp's own certificate chain. + * + * This is the convenience wrapper for the typical end-of-flow operation + * in a multi-signer advanced electronic signature (AdES) workflow. + * Equivalent to calling {@link addValidationData} followed by + * {@link addTimestamp} with `longTermValidation: true`. Note that unlike + * {@link addValidationData}, this adds a new signature object (the + * document timestamp field) to the PDF. + * + * If this method throws after partial progress (e.g. the TSA request + * fails after the DSS update was written), the in-memory instance may be + * out of sync with its bytes - discard it and reload from the last + * known-good bytes. + * + * @param options Archival options including the TSA + * @returns Bytes, warnings, and the number of pre-existing signatures + * for which LTV data was gathered + * + * @throws {SignatureError} If `timestampAuthority` is missing or the + * document cannot be saved incrementally (which would invalidate + * existing signatures) + * + * @example + * ```typescript + * import { HttpTimestampAuthority } from "@libpdf/core"; + * + * // After every recipient has signed (B-T), seal the document. + * const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + * const { bytes } = await pdf.addArchivalData({ timestampAuthority: tsa }); + * ``` + */ + async addArchivalData(options: ArchivalDataOptions): Promise { + const signature = new PDFSignature(this); + + return signature.addArchivalData(options); + } + // ───────────────────────────────────────────────────────────────────────────── // Layers (Optional Content Groups) // ───────────────────────────────────────────────────────────────────────────── @@ -3135,8 +3271,8 @@ export class PDF { securityHandler = handler; } } - // Note: action === "remove" means no encrypt dict (decrypted on load, written without encryption) + // Note: action === "remove" means no encrypt dict (decrypted on load, written without encryption) // Ensure document has an /ID (required for signatures, recommended for all PDFs) if (!fileId) { const idArray = this.ctx.info.trailer.getArray("ID"); diff --git a/src/document/forms/acro-form.ts b/src/document/forms/acro-form.ts index 6665d5a..f4721e3 100644 --- a/src/document/forms/acro-form.ts +++ b/src/document/forms/acro-form.ts @@ -583,7 +583,6 @@ export class AcroForm implements AcroFormLike { : partialName; // Check if terminal or non-terminal - if (this.isTerminalField(dict)) { const field = createFormField(dict, ref, this.registry, this, fullName); @@ -641,7 +640,6 @@ export class AcroForm implements AcroFormLike { // If first kid has /T, it's a child field → parent is non-terminal // If first kid has no /T, it's a widget → parent is terminal - return !firstKidDict.has("T"); } diff --git a/src/index.ts b/src/index.ts index 497d5ca..742e4c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,6 +104,8 @@ export { PermissionDeniedError, SecurityError } from "./security/errors"; // ───────────────────────────────────────────────────────────────────────────── export type { + ArchivalDataOptions, + ArchivalDataResult, DigestAlgorithm, HttpTimestampAuthorityOptions, KeyType, @@ -116,6 +118,10 @@ export type { SignWarning, SubFilter, TimestampAuthority, + TimestampOptions, + TimestampResult, + ValidationDataOptions, + ValidationDataResult, } from "./signatures"; export { CertificateChainError, diff --git a/src/integration/signatures/lta-finalization.test.ts b/src/integration/signatures/lta-finalization.test.ts new file mode 100644 index 0000000..ca2cc28 --- /dev/null +++ b/src/integration/signatures/lta-finalization.test.ts @@ -0,0 +1,312 @@ +/** + * Integration tests for `pdf.addValidationData()` and `pdf.addArchivalData()` + * — the end-of-flow PAdES B-LTA finalization operations used after a + * multi-signer advanced electronic signature (AdES) workflow. + */ + +import { PDF } from "#src/api/pdf"; +import { PdfRef } from "#src/objects/pdf-ref"; +import { PdfStream } from "#src/objects/pdf-stream"; +import { computeSha1Hex } from "#src/signatures/ltv/vri"; +import { HttpTimestampAuthority } from "#src/signatures/timestamp"; +import { loadFixture, saveTestOutput } from "#src/test-utils"; +import { describe, expect, it } from "vitest"; + +import { loadTestSigner, TEST_TSA_URL } from "./test-helpers"; + +describe("LTA finalization integration", () => { + const tsa = new HttpTimestampAuthority(TEST_TSA_URL); + + describe("addValidationData()", () => { + it("is a no-op on an unsigned PDF", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes, warnings, signatureCount } = await pdf.addValidationData(); + + expect(signatureCount).toBe(0); + expect(warnings).toHaveLength(0); + + // No signatures → no DSS in the resulting bytes. + const pdfStr = new TextDecoder().decode(bytes); + expect(pdfStr).not.toContain("/Type /DSS"); + }); + + it("upgrades a B-T signature to B-LT", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // Sign at B-T (no LTV embedded yet). + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + }); + + // Pre-condition: no DSS yet. + const beforeStr = new TextDecoder().decode(await pdf.save()); + expect(beforeStr).not.toContain("/Type /DSS"); + + const { bytes, warnings, signatureCount } = await pdf.addValidationData(); + + expect(signatureCount).toBe(1); + + const pdfStr = new TextDecoder().decode(bytes); + expect(pdfStr).toContain("/Type /DSS"); + expect(pdfStr).toContain("/Certs"); + expect(pdfStr).toContain("/VRI"); + + // Should be safe even if revocation lookups produce CHAIN_INCOMPLETE + // warnings — we only assert that no LTV_GATHER_FAILED occurred. + const fatal = warnings.filter(w => w.code === "LTV_GATHER_FAILED"); + expect(fatal).toHaveLength(0); + + await saveTestOutput("signatures/validation-data-upgraded.pdf", bytes); + }); + + it("gathers LTV for multiple signatures with one DSS write", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // Two B-T signatures. + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer1", + }); + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer2", + }); + + const { bytes, signatureCount } = await pdf.addValidationData(); + + expect(signatureCount).toBe(2); + + const pdfStr = new TextDecoder().decode(bytes); + + // One DSS, two VRI entries (one per signature). + expect(pdfStr).toContain("/Type /DSS"); + + // The DSS update should be a single incremental revision — count + // xref sections: original + 2 signatures + 1 DSS = 4. + const xrefCount = (pdfStr.match(/^xref$/gm) ?? []).length; + expect(xrefCount).toBe(4); + + await saveTestOutput("signatures/validation-data-multi-signer.pdf", bytes); + }); + + it("merges with pre-existing DSS data without duplicating certs", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // First sign at B-LT (writes DSS for signer 1). + await pdf.sign({ + signer, + level: "B-LT", + timestampAuthority: tsa, + fieldName: "Signer1", + }); + + // Then add another B-T sig (no DSS). + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer2", + }); + + const { bytes, signatureCount } = await pdf.addValidationData(); + + expect(signatureCount).toBe(2); + + const pdfStr = new TextDecoder().decode(bytes); + + // Both signature fields are still present. + expect(pdfStr).toContain("/T (Signer1)"); + expect(pdfStr).toContain("/T (Signer2)"); + + // Walk catalog → DSS → VRI in the merged result: the pre-existing + // VRI entry (from the B-LT sign) must survive the merge and the new + // signature must have its own entry. Both signatures use the same + // TSA, whose token gets its own VRI entry, so expect at least 3. + const reloaded = await PDF.load(bytes); + const resolve = reloaded.context.registry.resolve.bind(reloaded.context.registry); + const dss = reloaded.getCatalog().getDict("DSS", resolve); + + expect(dss).toBeDefined(); + + const vri = dss?.getDict("VRI", resolve); + + expect(vri).toBeDefined(); + expect([...(vri?.keys() ?? [])].length).toBeGreaterThanOrEqual(3); + + // Certs must be deduplicated: both signatures share the same signer + // chain, so no two /Certs streams may contain identical bytes. + const certs = dss?.getArray("Certs", resolve); + + expect(certs).toBeDefined(); + expect(certs && certs.length).toBeGreaterThan(0); + + const certHashes = new Set(); + + for (const item of certs ?? []) { + const stream = item instanceof PdfRef ? resolve(item) : item; + + expect(stream).toBeInstanceOf(PdfStream); + + if (stream instanceof PdfStream) { + const hash = await computeSha1Hex(stream.getDecodedData()); + + expect(certHashes.has(hash)).toBe(false); + certHashes.add(hash); + } + } + }); + }); + + describe("addArchivalData()", () => { + it("performs full B-LTA on a single B-T signature", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + }); + + const { bytes, warnings, signatureCount } = await pdf.addArchivalData({ + timestampAuthority: tsa, + }); + + expect(signatureCount).toBe(1); + + const pdfStr = new TextDecoder().decode(bytes); + + // Original signature + DSS + DocTimeStamp + DSS for timestamp. + expect(pdfStr).toContain("/Type /Sig"); + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/SubFilter /ETSI.RFC3161"); + expect(pdfStr).toContain("/Type /DSS"); + + // No fatal warnings. + const fatal = warnings.filter(w => w.code === "LTV_GATHER_FAILED"); + expect(fatal).toHaveLength(0); + + await saveTestOutput("signatures/archival-single-signer.pdf", bytes); + }); + + it("performs full B-LTA on a multi-signer document", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // Three B-T signatures — typical multi-recipient AdES flow. + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer1", + }); + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer2", + }); + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer3", + }); + + const { bytes, warnings, signatureCount } = await pdf.addArchivalData({ + timestampAuthority: tsa, + }); + + // The three pre-existing signatures get LTV gathered. + expect(signatureCount).toBe(3); + + const pdfStr = new TextDecoder().decode(bytes); + + // All three signatures present. + expect(pdfStr).toContain("/T (Signer1)"); + expect(pdfStr).toContain("/T (Signer2)"); + expect(pdfStr).toContain("/T (Signer3)"); + + // Archival timestamp + DSS present. + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/Type /DSS"); + + // No fatal warnings. + const fatal = warnings.filter(w => w.code === "LTV_GATHER_FAILED"); + expect(fatal).toHaveLength(0); + + await saveTestOutput("signatures/archival-multi-signer.pdf", bytes); + }); + + it("uses a pre-allocated field for the archival timestamp", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // Reserve archival timestamp field up front, then sign normally. + pdf.getOrCreateForm().createSignatureField("ArchivalTS"); + await pdf.reload(await pdf.save()); + + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + fieldName: "Signer1", + }); + + const { bytes } = await pdf.addArchivalData({ + timestampAuthority: tsa, + fieldName: "ArchivalTS", + }); + + const pdfStr = new TextDecoder().decode(bytes); + + // The reserved field is filled. + expect(pdfStr).toContain("/T (ArchivalTS)"); + // Auto-generated timestamp name should not have been used. + expect(pdfStr).not.toContain("/T (Timestamp_1)"); + }); + + it("throws when timestampAuthority is omitted", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + await expect( + // oxlint-disable-next-line typescript/no-explicit-any + pdf.addArchivalData({} as any), + ).rejects.toThrow(/timestampAuthority/); + }); + + it("works on an unsigned PDF (just adds a timestamp + its DSS)", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes, signatureCount } = await pdf.addArchivalData({ + timestampAuthority: tsa, + }); + + // No pre-existing sigs, but the timestamp itself is still embedded. + expect(signatureCount).toBe(0); + + const pdfStr = new TextDecoder().decode(bytes); + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/Type /DSS"); + }); + }); +}); diff --git a/src/integration/signatures/signing.test.ts b/src/integration/signatures/signing.test.ts index 8f13225..21bca19 100644 --- a/src/integration/signatures/signing.test.ts +++ b/src/integration/signatures/signing.test.ts @@ -11,32 +11,9 @@ import { HttpTimestampAuthority } from "#src/signatures/timestamp"; import { loadFixture, saveTestOutput } from "#src/test-utils"; import { describe, expect, it } from "vitest"; -/** Test P12 files with different encryption formats */ -const P12_FILES = { - /** AES-256-CBC (modern default) */ - aes256: "test-signer-aes256.p12", - /** AES-128-CBC */ - aes128: "test-signer-aes128.p12", - /** Triple DES (legacy but common) */ - tripleDes: "test-signer-3des.p12", - /** RC2-40 (very old legacy format) */ - legacy: "test-signer-rc2-40.p12", - /** ECDSA P-256 */ - ecdsaP256: "test-signer-ec-p256-aes256.p12", - /** ECDSA P-384 */ - ecdsaP384: "test-signer-ec-p384-aes256.p12", -}; +import { loadTestSigner, P12_FILES, TEST_TSA_URL } from "./test-helpers"; describe("signing integration", () => { - /** - * Load the test P12 certificate (default AES-256). - */ - async function loadTestSigner(filename = P12_FILES.aes256) { - const p12Bytes = await loadFixture("certificates", filename); - - return P12Signer.create(p12Bytes, "test123"); - } - describe("B-B signing (basic)", () => { it("signs a simple PDF document", async () => { const pdfBytes = await loadFixture("basic", "rot0.pdf"); @@ -124,7 +101,7 @@ describe("signing integration", () => { describe("B-T signing (with timestamp)", () => { // FreeTSA is a free public timestamp authority - const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + const tsa = new HttpTimestampAuthority(TEST_TSA_URL); it("signs with timestamp (B-T level)", async () => { const pdfBytes = await loadFixture("basic", "rot0.pdf"); @@ -168,7 +145,7 @@ describe("signing integration", () => { describe("B-LT signing (long-term validation)", () => { // FreeTSA is a free public timestamp authority - const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + const tsa = new HttpTimestampAuthority(TEST_TSA_URL); it("signs with timestamp and LTV data (B-LT level)", async () => { const pdfBytes = await loadFixture("basic", "rot0.pdf"); diff --git a/src/integration/signatures/test-helpers.ts b/src/integration/signatures/test-helpers.ts new file mode 100644 index 0000000..f30af6f --- /dev/null +++ b/src/integration/signatures/test-helpers.ts @@ -0,0 +1,34 @@ +/** + * Shared helpers for signature integration tests. + */ + +import { P12Signer } from "#src/signatures/signers"; +import { loadFixture } from "#src/test-utils"; + +/** Public RFC 3161 timestamp authority used by integration tests. */ +export const TEST_TSA_URL = "https://freetsa.org/tsr"; + +/** Test P12 files with different encryption formats */ +export const P12_FILES = { + /** AES-256-CBC (modern default) */ + aes256: "test-signer-aes256.p12", + /** AES-128-CBC */ + aes128: "test-signer-aes128.p12", + /** Triple DES (legacy but common) */ + tripleDes: "test-signer-3des.p12", + /** RC2-40 (very old legacy format) */ + legacy: "test-signer-rc2-40.p12", + /** ECDSA P-256 */ + ecdsaP256: "test-signer-ec-p256-aes256.p12", + /** ECDSA P-384 */ + ecdsaP384: "test-signer-ec-p384-aes256.p12", +}; + +/** + * Load a test P12 signer (default AES-256). + */ +export async function loadTestSigner(filename: string = P12_FILES.aes256): Promise { + const p12Bytes = await loadFixture("certificates", filename); + + return P12Signer.create(p12Bytes, "test123"); +} diff --git a/src/integration/signatures/timestamping.test.ts b/src/integration/signatures/timestamping.test.ts new file mode 100644 index 0000000..9cb0b36 --- /dev/null +++ b/src/integration/signatures/timestamping.test.ts @@ -0,0 +1,302 @@ +/** + * Integration tests for `pdf.addTimestamp()` — archival document timestamps. + * + * These tests use FreeTSA (a public RFC 3161 timestamp authority) to exercise + * the PAdES B-LTA flow where document timestamps are appended after one or + * more signatures. + */ + +import { PDF } from "#src/api/pdf"; +import { HttpTimestampAuthority } from "#src/signatures/timestamp"; +import { loadFixture, saveTestOutput } from "#src/test-utils"; +import { describe, expect, it } from "vitest"; + +import { loadTestSigner, TEST_TSA_URL } from "./test-helpers"; + +describe("timestamping integration", () => { + const tsa = new HttpTimestampAuthority(TEST_TSA_URL); + + describe("standalone document timestamp", () => { + it("adds a document timestamp to an unsigned PDF", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes, warnings } = await pdf.addTimestamp({ + timestampAuthority: tsa, + }); + + // Should produce a valid, larger PDF. + expect(bytes.length).toBeGreaterThan(pdfBytes.length); + expect(new TextDecoder().decode(bytes.slice(0, 5))).toBe("%PDF-"); + expect(warnings).toHaveLength(0); + + const pdfStr = new TextDecoder().decode(bytes); + + // Should contain a document timestamp dictionary with the right shape. + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/Filter /Adobe.PPKLite"); + expect(pdfStr).toContain("/SubFilter /ETSI.RFC3161"); + + // Default field name uses the Timestamp_ prefix. + expect(pdfStr).toContain("/T (Timestamp_1)"); + + // AcroForm /SigFlags must be set so viewers recognize the timestamp. + expect(pdfStr).toMatch(/\/SigFlags\s+3/); + + // Incremental update markers must be present. + expect(pdfStr).toContain("/Prev"); + expect(pdfStr.trim()).toMatch(/%%EOF\s*$/); + + await saveTestOutput("signatures/timestamped-unsigned.pdf", bytes); + }); + + it("appends an archival timestamp after a B-T signature", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + const signer = await loadTestSigner(); + + // First sign with B-T (timestamped signature). + await pdf.sign({ + signer, + level: "B-T", + timestampAuthority: tsa, + reason: "Approval", + }); + + // Then seal with an archival document timestamp. + const { bytes, warnings } = await pdf.addTimestamp({ + timestampAuthority: tsa, + }); + + expect(warnings).toHaveLength(0); + + const pdfStr = new TextDecoder().decode(bytes); + + // Both the signature and the document timestamp must be present. + expect(pdfStr).toContain("/Type /Sig"); + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/SubFilter /ETSI.CAdES.detached"); + expect(pdfStr).toContain("/SubFilter /ETSI.RFC3161"); + + // Two incremental updates means at least three xref sections + // (original + signature + timestamp). + const xrefCount = (pdfStr.match(/^xref$/gm) ?? []).length; + expect(xrefCount).toBeGreaterThanOrEqual(3); + + await saveTestOutput("signatures/signed-then-timestamped.pdf", bytes); + }); + + it("uses a custom field name when provided", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes } = await pdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "ArchivalTS", + }); + + const pdfStr = new TextDecoder().decode(bytes); + + expect(pdfStr).toContain("/T (ArchivalTS)"); + expect(pdfStr).not.toContain("/T (Timestamp_1)"); + }); + + it("produces a non-empty /Contents value covering the document", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes } = await pdf.addTimestamp({ timestampAuthority: tsa }); + + const pdfStr = new TextDecoder().decode(bytes); + + // The /Contents must contain a real timestamp token, not just zeros. + const contentsMatches = [...pdfStr.matchAll(/\/Contents\s*<([0-9A-Fa-f]+)>/g)]; + expect(contentsMatches.length).toBeGreaterThan(0); + + const lastContentsHex = contentsMatches.at(-1)?.[1] ?? ""; + expect(lastContentsHex).not.toMatch(/^0+$/); + expect(lastContentsHex.length).toBeGreaterThan(1000); + + // ByteRange should cover the whole file outside the /Contents window. + const byteRangeMatch = pdfStr.match(/\/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/); + expect(byteRangeMatch).not.toBeNull(); + + const [, offset1, length1, offset2, length2] = byteRangeMatch?.map(Number) ?? []; + + expect(offset1).toBe(0); + expect(offset2).toBeGreaterThan(length1); + expect(offset2 + length2).toBe(bytes.length); + }); + }); + + describe("pre-allocated timestamp field (DocMDP / AES flows)", () => { + it("fills a pre-allocated empty signature field by name", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + // Reserve the timestamp field up-front, before any signing happens. + // In a real DocMDP flow this would be done during document + // preparation so the /AcroForm /Fields array is locked in before + // the certification signature is applied. + const form = pdf.getOrCreateForm(); + form.createSignatureField("ArchivalTimestamp"); + const prepared = await pdf.save(); + const preparedPdf = await PDF.load(prepared); + + const { bytes, warnings } = await preparedPdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "ArchivalTimestamp", + }); + + expect(warnings).toHaveLength(0); + + const pdfStr = new TextDecoder().decode(bytes); + + // The reserved field is used; no new Timestamp_N field is created. + expect(pdfStr).toContain("/T (ArchivalTimestamp)"); + expect(pdfStr).not.toContain("/T (Timestamp_1)"); + + // And it is a real document timestamp, not a regular signature. + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/SubFilter /ETSI.RFC3161"); + + await saveTestOutput("signatures/timestamped-prealloc.pdf", bytes); + }); + + it("rejects reusing a field that is not a signature field", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const form = pdf.getOrCreateForm(); + form.createTextField("NotASig"); + const prepared = await pdf.save(); + const preparedPdf = await PDF.load(prepared); + + await expect( + preparedPdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "NotASig", + }), + ).rejects.toThrow(/not a signature field/); + }); + }); + + describe("multiple appended timestamps", () => { + it("can stack multiple archival timestamps on the same instance", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + await pdf.addTimestamp({ timestampAuthority: tsa }); + const { bytes } = await pdf.addTimestamp({ timestampAuthority: tsa }); + + const pdfStr = new TextDecoder().decode(bytes); + + // Each timestamp gets its own field; default names are generated. + expect(pdfStr).toContain("/T (Timestamp_1)"); + expect(pdfStr).toContain("/T (Timestamp_2)"); + + // Two timestamps → at least three xref sections. + const xrefCount = (pdfStr.match(/^xref$/gm) ?? []).length; + expect(xrefCount).toBeGreaterThanOrEqual(3); + + await saveTestOutput("signatures/timestamped-twice.pdf", bytes); + }); + }); + + describe("long-term validation", () => { + it("embeds DSS data for the timestamp's certificate chain", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes, warnings } = await pdf.addTimestamp({ + timestampAuthority: tsa, + longTermValidation: true, + }); + + // Network-flakiness may produce warnings; structure must still be right. + const pdfStr = new TextDecoder().decode(bytes); + + expect(pdfStr).toContain("/Type /DocTimeStamp"); + expect(pdfStr).toContain("/Type /DSS"); + expect(pdfStr).toContain("/Certs"); + + // VRI entry must be present for the timestamp's /Contents value. + expect(pdfStr).toContain("/VRI"); + + if (warnings.length > 0) { + console.log("timestamp LTV warnings:", warnings); + } + + await saveTestOutput("signatures/timestamped-ltv.pdf", bytes); + }); + }); + + describe("loaded after timestamping", () => { + it("the timestamped PDF can be re-parsed", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes } = await pdf.addTimestamp({ timestampAuthority: tsa }); + + const reloaded = await PDF.load(bytes); + expect(reloaded.getPageCount()).toBe(pdf.getPageCount()); + }); + + it("preserves the original bytes at the head of the file", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const { bytes } = await pdf.addTimestamp({ timestampAuthority: tsa }); + + const originalPrefix = bytes.slice(0, 100); + const expectedPrefix = pdfBytes.slice(0, 100); + expect(originalPrefix).toEqual(expectedPrefix); + }); + }); + + describe("error handling", () => { + it("throws when timestampAuthority is omitted", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + await expect( + // oxlint-disable-next-line typescript/no-explicit-any + pdf.addTimestamp({} as any), + ).rejects.toThrow(/timestampAuthority/); + }); + + it("throws when the requested field name is already signed", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + await pdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "MyTimestamp", + }); + + // The first call already filled MyTimestamp; a second call against + // the same name must refuse to overwrite an already-signed field. + await expect( + pdf.addTimestamp({ + timestampAuthority: tsa, + fieldName: "MyTimestamp", + }), + ).rejects.toThrow(/already signed/); + }); + + it("propagates errors from the timestamp authority", async () => { + const pdfBytes = await loadFixture("basic", "rot0.pdf"); + const pdf = await PDF.load(pdfBytes); + + const failingTsa = { + async timestamp(): Promise { + throw new Error("TSA unavailable"); + }, + }; + + await expect(pdf.addTimestamp({ timestampAuthority: failingTsa })).rejects.toThrow( + /TSA unavailable/, + ); + }); + }); +}); diff --git a/src/signatures/index.ts b/src/signatures/index.ts index e694e64..c14a275 100644 --- a/src/signatures/index.ts +++ b/src/signatures/index.ts @@ -42,6 +42,8 @@ export { CryptoKeySigner, GoogleKmsSigner, P12Signer, type P12SignerOptions } fr export { HttpTimestampAuthority, type HttpTimestampAuthorityOptions } from "./timestamp"; // Types export type { + ArchivalDataOptions, + ArchivalDataResult, DigestAlgorithm, KeyType, LtvValidationData, @@ -54,6 +56,10 @@ export type { SignWarning, SubFilter, TimestampAuthority, + TimestampOptions, + TimestampResult, + ValidationDataOptions, + ValidationDataResult, } from "./types"; // Errors export { diff --git a/src/signatures/ltv/dss-builder.ts b/src/signatures/ltv/dss-builder.ts index 7946da3..c82940c 100644 --- a/src/signatures/ltv/dss-builder.ts +++ b/src/signatures/ltv/dss-builder.ts @@ -9,7 +9,7 @@ */ import type { ObjectRegistry } from "#src/document/object-registry.ts"; -import { formatPdfDate } from "#src/helpers/format.ts"; +import { formatPdfDate, parsePdfDate } from "#src/helpers/format.ts"; import { PdfArray } from "#src/objects/pdf-array.ts"; import { PdfDict } from "#src/objects/pdf-dict.ts"; import { PdfName } from "#src/objects/pdf-name.ts"; @@ -131,13 +131,12 @@ export class DSSBuilder { const ocspHashes = await builder.extractRefHashes(entry, "OCSP", builder.ocspMap); const crlHashes = await builder.extractRefHashes(entry, "CRL", builder.crlMap); - // Get timestamp if present + // Preserve the original VRI creation time if present let timestamp: Date | undefined; const tuVal = entry.getString("TU", resolve); if (tuVal) { - // Parse PDF date format - simplified - timestamp = new Date(); + timestamp = parsePdfDate(tuVal.asString()); } // VRI keys are PdfName, need to get the value string diff --git a/src/signatures/ltv/gatherer.ts b/src/signatures/ltv/gatherer.ts index 1211eb7..67cf413 100644 --- a/src/signatures/ltv/gatherer.ts +++ b/src/signatures/ltv/gatherer.ts @@ -260,7 +260,6 @@ export class LtvDataGatherer { // ASN.1 DER: first byte is tag, second+ bytes are length // If length < 128, it's a single byte // If length >= 128, high bit is set and low bits indicate how many length bytes follow - const lengthByte = bytes[1]; if (lengthByte < 128) { diff --git a/src/signatures/types.ts b/src/signatures/types.ts index 9f6393a..232f771 100644 --- a/src/signatures/types.ts +++ b/src/signatures/types.ts @@ -245,6 +245,174 @@ export interface SignOptions { estimatedSize?: number; } +// ───────────────────────────────────────────────────────────────────────────── +// Timestamp Options +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Options for adding an archival document timestamp to a PDF. + * + * A document timestamp creates a `/Type /DocTimeStamp` signature dictionary + * whose ByteRange covers the entire current document, extending the validity + * of any existing signatures. This is the timestamping step used at the end + * of a PAdES B-LTA flow when signatures have been appended and the caller + * wants to seal the document with a trusted time. + * + * @example + * ```typescript + * const { bytes, warnings } = await pdf.addTimestamp({ + * timestampAuthority: new HttpTimestampAuthority("https://freetsa.org/tsr"), + * longTermValidation: true, + * }); + * ``` + */ +export interface TimestampOptions { + /** RFC 3161 timestamp authority used to obtain the timestamp token. */ + timestampAuthority: TimestampAuthority; + + /** + * Signature field name to use for the document timestamp. + * + * Behavior: + * - If omitted, a unique name with the `Timestamp_` prefix is generated + * and a fresh field is created. + * - If provided and the name matches an existing **unsigned** signature + * field, that pre-allocated field is reused (the timestamp's ref is + * written into its `/V`). This is the recommended pattern for + * multi-signer advanced electronic signature (AdES) / DocMDP flows + * where the document author reserves signature fields up front before + * the certification signature is applied. + * - If the name matches a signed signature field, or a non-signature + * field, an error is thrown. + * - If the name doesn't match any existing field, a new field is created. + */ + fieldName?: string; + + /** + * Embed long-term validation data (certificates, OCSP responses, CRLs) + * for the timestamp's certificate chain in the DSS. + * + * Enable this for PAdES B-LTA semantics where the timestamp itself + * needs to remain verifiable after its TSA certificate expires. + * + * @default false + */ + longTermValidation?: boolean; + + /** Provider for OCSP/CRL data when `longTermValidation` is true. */ + revocationProvider?: RevocationProvider; + + /** + * Digest algorithm used to hash the document for the TSA request. + * + * @default "SHA-256" + */ + digestAlgorithm?: DigestAlgorithm; + + /** + * Size to reserve for the timestamp placeholder in bytes. + * + * Must be large enough to hold the TSA's timestamp token (the CMS + * structure plus the full TSA certificate chain). + * + * @default 12288 (12KB) + */ + estimatedSize?: number; +} + +/** + * Result of `addTimestamp()`. + */ +export interface TimestampResult { + /** The PDF bytes with the document timestamp embedded. */ + bytes: Uint8Array; + + /** Warnings emitted during the timestamping operation. */ + warnings: SignWarning[]; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Validation Data Options (DSS-only update) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Options for `pdf.addValidationData()`. + * + * Walks every signed signature field (regular signatures and document + * timestamps), gathers certificates / OCSP responses / CRLs for each one, + * and writes a single DSS incremental update that contains all of it. + * + * Upgrades B-T signatures in the document to B-LT. Does not add a + * timestamp — for that, use `pdf.addTimestamp()` or `pdf.addArchivalData()`. + */ +export interface ValidationDataOptions { + /** + * Provider for OCSP/CRL data. + * + * @default new DefaultRevocationProvider() + */ + revocationProvider?: RevocationProvider; +} + +/** + * Result of `addValidationData()`. + */ +export interface ValidationDataResult { + /** The PDF bytes with the DSS update embedded. */ + bytes: Uint8Array; + + /** Warnings emitted during gathering (e.g. CHAIN_INCOMPLETE, REVOCATION_UNAVAILABLE). */ + warnings: SignWarning[]; + + /** Number of signed fields for which LTV data was gathered. */ + signatureCount: number; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Archival Data Options (full B-LTA finalization) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Options for `pdf.addArchivalData()`. + * + * Convenience wrapper that performs full PAdES B-LTA finalization in a + * single call: + * + * 1. Gathers LTV data for every existing signed field (DSS update) + * 2. Adds an archival `/DocTimeStamp` + * 3. Gathers LTV data for the new timestamp's own certificate chain + * + * Use this at the end of a multi-signer flow once every recipient has + * signed and you want to seal the document with a trusted time + full + * long-term validation data. + * + * The options match {@link TimestampOptions} except that long-term + * validation is always enabled for the archival timestamp. + * + * @example + * ```typescript + * const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr"); + * const { bytes, warnings, signatureCount } = await pdf.addArchivalData({ + * timestampAuthority: tsa, + * }); + * ``` + */ +export type ArchivalDataOptions = Omit; + +/** + * Result of `addArchivalData()`. + */ +export interface ArchivalDataResult { + /** The PDF bytes after the DSS update + archival timestamp. */ + bytes: Uint8Array; + + /** Warnings emitted during the operation. */ + warnings: SignWarning[]; + + /** Number of pre-existing signed fields for which LTV data was gathered. */ + signatureCount: number; +} + // ───────────────────────────────────────────────────────────────────────────── // Sign Result // ───────────────────────────────────────────────────────────────────────────── From 838373a4cdd1337ed3ff0edc3fdd39fa37da7bc7 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 12 Jun 2026 14:34:34 +1000 Subject: [PATCH 2/3] fix(forms): clean up structure tree references when flattening Tagged PDFs reference widget annotations from the structure tree via /OBJR entries and the /ParentTree. Flatten left these intact, keeping the removed widgets reachable so full-save GC wrote all the orphaned field objects back into the output. Remove /OBJR kids and stale /ParentTree entries for flattened widgets while preserving references to non-widget annotations. --- src/document/forms/acro-form.test.ts | 173 ++++++++++++++++++- src/document/forms/acro-form.ts | 13 +- src/document/forms/form-flattener.ts | 30 +++- src/document/struct-tree.ts | 245 +++++++++++++++++++++++++++ 4 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 src/document/struct-tree.ts diff --git a/src/document/forms/acro-form.test.ts b/src/document/forms/acro-form.test.ts index 541fe1a..d20ae6c 100644 --- a/src/document/forms/acro-form.test.ts +++ b/src/document/forms/acro-form.test.ts @@ -1,5 +1,10 @@ import { PDF } from "#src/api/pdf"; -import { loadFixture } from "#src/test-utils"; +import { PdfArray } from "#src/objects/pdf-array"; +import { PdfDict } from "#src/objects/pdf-dict"; +import { PdfName } from "#src/objects/pdf-name"; +import { PdfNumber } from "#src/objects/pdf-number"; +import type { PdfObject } from "#src/objects/pdf-object"; +import { loadFixture, toAsciiString } from "#src/test-utils"; import { describe, expect, it } from "vitest"; import { AcroForm } from "./acro-form"; @@ -1012,6 +1017,172 @@ describe("Form Writing", () => { }); }); + describe("flatten with tagged PDF (structure tree)", () => { + /** + * Build a structure tree that references every widget via /OBJR entries, + * plus one non-widget (Link) annotation, mirroring how tagged (accessible) + * forms reference their fields. Without struct tree cleanup, these /OBJR + * references keep the removed widgets reachable, so a full save writes + * all the orphaned field objects back into the output. + */ + async function loadTaggedForm(): Promise<{ pdf: PDF; form: AcroForm; linkKey: number }> { + const bytes = await loadFixture("forms", "form_to_flatten.pdf"); + const pdf = await PDF.load(bytes); + const registry = pdf.context.registry; + const resolve = registry.resolve.bind(registry); + const catalogDict = pdf.context.catalog.getDict(); + const form = pdf.getForm()!.acroForm(); + + const docKids = new PdfArray([]); + const docElem = PdfDict.of({ S: PdfName.of("Document"), K: docKids }); + const docElemRef = registry.register(docElem); + + const nums: PdfObject[] = []; + let nextKey = 0; + + for (const field of form.getFields()) { + for (const widget of field.getWidgets()) { + if (!widget.ref) { + continue; + } + + const objr = PdfDict.of({ Type: PdfName.of("OBJR"), Obj: widget.ref }); + const elem = PdfDict.of({ + S: PdfName.of("Form"), + P: docElemRef, + K: PdfArray.of(registry.register(objr)), + }); + const elemRef = registry.register(elem); + + docKids.push(elemRef); + widget.dict.set("StructParent", new PdfNumber(nextKey)); + nums.push(new PdfNumber(nextKey), elemRef); + nextKey++; + } + } + + expect(nextKey).toBeGreaterThan(0); + + // Add a non-widget (Link) annotation referenced from the struct tree. + // It must survive flattening untouched. + const pageRef = pdf.context.pages.getPage(0)!; + const pageDict = resolve(pageRef) as PdfDict; + const linkKey = nextKey; + const link = PdfDict.of({ + Type: PdfName.of("Annot"), + Subtype: PdfName.of("Link"), + Rect: PdfArray.of(new PdfNumber(0), new PdfNumber(0), new PdfNumber(10), new PdfNumber(10)), + StructParent: new PdfNumber(linkKey), + }); + const linkRef = registry.register(link); + const annots = pageDict.getArray("Annots", resolve); + + if (annots) { + annots.push(linkRef); + } else { + pageDict.set("Annots", PdfArray.of(linkRef)); + } + + const linkObjr = PdfDict.of({ Type: PdfName.of("OBJR"), Obj: linkRef }); + const linkElem = PdfDict.of({ + S: PdfName.of("Link"), + P: docElemRef, + K: PdfArray.of(registry.register(linkObjr)), + }); + const linkElemRef = registry.register(linkElem); + + docKids.push(linkElemRef); + nums.push(new PdfNumber(linkKey), linkElemRef); + nextKey++; + + const parentTree = PdfDict.of({ Nums: new PdfArray(nums) }); + const structTreeRoot = PdfDict.of({ + Type: PdfName.of("StructTreeRoot"), + K: docElemRef, + ParentTree: registry.register(parentTree), + ParentTreeNextKey: new PdfNumber(nextKey), + }); + + catalogDict.set("StructTreeRoot", registry.register(structTreeRoot)); + + return { pdf, form, linkKey }; + } + + it("does not leave orphaned field objects in the saved output", async () => { + const { pdf, form } = await loadTaggedForm(); + + form.flatten(); + + const saved = await pdf.save(); + const text = toAsciiString(saved, saved.length); + + // No field dicts should survive the full-save garbage collection + expect(text).not.toContain("/FT"); + expect(text).not.toContain("/Widget"); + + // Only the Link annotation's OBJR should remain + expect(text.match(/\/OBJR/g) ?? []).toHaveLength(1); + }); + + it("removes stale ParentTree entries for flattened widgets", async () => { + const { pdf, form, linkKey } = await loadTaggedForm(); + + form.flatten(); + + const saved = await pdf.save(); + const pdf2 = await PDF.load(saved); + const resolve2 = pdf2.context.registry.resolve.bind(pdf2.context.registry); + const catalog2 = pdf2.context.catalog.getDict(); + + const structTreeRoot = catalog2.getDict("StructTreeRoot", resolve2); + expect(structTreeRoot).toBeDefined(); + + const parentTree = structTreeRoot!.getDict("ParentTree", resolve2); + const numsAfter = parentTree!.getArray("Nums", resolve2)!; + + // Only the Link annotation's entry should remain + const keys: number[] = []; + + for (let i = 0; i + 1 < numsAfter.length; i += 2) { + const key = numsAfter.at(i, resolve2); + + if (key instanceof PdfNumber) { + keys.push(key.value); + } + } + + expect(keys).toEqual([linkKey]); + }); + + it("preserves struct tree references to non-widget annotations", async () => { + const { pdf, form } = await loadTaggedForm(); + + form.flatten(); + + const saved = await pdf.save(); + const pdf2 = await PDF.load(saved); + const resolve2 = pdf2.context.registry.resolve.bind(pdf2.context.registry); + + // The Link annotation should still be on the page + const pageRef = pdf2.context.pages.getPage(0)!; + const pageDict = resolve2(pageRef) as PdfDict; + const annots = pageDict.getArray("Annots", resolve2); + expect(annots).toBeDefined(); + + let linkFound = false; + + for (let i = 0; i < annots!.length; i++) { + const annot = annots!.at(i, resolve2); + + if (annot instanceof PdfDict && annot.getName("Subtype")?.value === "Link") { + linkFound = true; + } + } + + expect(linkFound).toBe(true); + }); + }); + describe("font management", () => { it("sets and gets default font", async () => { const bytes = await loadFixture("forms", "sample_form.pdf"); diff --git a/src/document/forms/acro-form.ts b/src/document/forms/acro-form.ts index f4721e3..da0a57c 100644 --- a/src/document/forms/acro-form.ts +++ b/src/document/forms/acro-form.ts @@ -37,6 +37,7 @@ export class AcroForm implements AcroFormLike { private readonly dict: PdfDict; private readonly registry: ObjectRegistry; private readonly pageTree: PDFPageTree | null; + private readonly catalog: PdfDict | null; private fieldsCache: TerminalField[] | null = null; @@ -49,10 +50,16 @@ export class AcroForm implements AcroFormLike { /** Cache of existing fonts from /DR */ private existingFontsCache: Map | null = null; - private constructor(dict: PdfDict, registry: ObjectRegistry, pageTree: PDFPageTree | null) { + private constructor( + dict: PdfDict, + registry: ObjectRegistry, + pageTree: PDFPageTree | null, + catalog: PdfDict | null, + ) { this.dict = dict; this.registry = registry; this.pageTree = pageTree; + this.catalog = catalog; } /** @@ -71,7 +78,7 @@ export class AcroForm implements AcroFormLike { return null; } - return new AcroForm(dict, registry, pageTree ?? null); + return new AcroForm(dict, registry, pageTree ?? null, catalog); } /** @@ -791,7 +798,7 @@ export class AcroForm implements AcroFormLike { * @param options Flattening options */ flatten(options: FlattenOptions = {}): void { - const flattener = new FormFlattener(this, this.registry, this.pageTree); + const flattener = new FormFlattener(this, this.registry, this.pageTree, this.catalog); flattener.flatten(options); diff --git a/src/document/forms/form-flattener.ts b/src/document/forms/form-flattener.ts index 07ca472..d0301e6 100644 --- a/src/document/forms/form-flattener.ts +++ b/src/document/forms/form-flattener.ts @@ -31,6 +31,7 @@ import { PdfRef } from "#src/objects/pdf-ref"; import { PdfStream } from "#src/objects/pdf-stream"; import type { ObjectRegistry } from "../object-registry"; +import { removeAnnotationsFromStructTree } from "../struct-tree"; import { SignatureField, type TerminalField } from "./fields"; import type { FormFont } from "./form-font"; import type { WidgetAnnotation } from "./widget-annotation"; @@ -95,11 +96,18 @@ export class FormFlattener { private readonly form: FlattenableForm; private readonly registry: ObjectRegistry; private readonly pageTree: PageTreeAccess | null; - - constructor(form: FlattenableForm, registry: ObjectRegistry, pageTree: PageTreeAccess | null) { + private readonly catalog: PdfDict | null; + + constructor( + form: FlattenableForm, + registry: ObjectRegistry, + pageTree: PageTreeAccess | null, + catalog: PdfDict | null = null, + ) { this.form = form; this.registry = registry; this.pageTree = pageTree; + this.catalog = catalog; } /** @@ -151,6 +159,24 @@ export class FormFlattener { this.flattenWidgetsOnPage(pageRef, widgets); } + // Remove structure tree references (/OBJR kids and /ParentTree entries) + // to the flattened widgets. In tagged PDFs the structure tree references + // widget annotations, which would otherwise keep the removed fields + // reachable and cause full saves to write them back as orphan objects. + if (this.catalog) { + const removedWidgetRefs = new Set(); + + for (const field of fieldsToFlatten) { + for (const widget of field.getWidgets()) { + if (widget.ref) { + removedWidgetRefs.add(widget.ref); + } + } + } + + removeAnnotationsFromStructTree(this.catalog, this.registry, removedWidgetRefs); + } + // Update form structure const dict = this.form.getDict(); diff --git a/src/document/struct-tree.ts b/src/document/struct-tree.ts new file mode 100644 index 0000000..f21ba08 --- /dev/null +++ b/src/document/struct-tree.ts @@ -0,0 +1,245 @@ +/** + * Structure tree maintenance for annotation removal. + * + * Tagged (accessible) PDFs reference annotations from the logical structure + * tree via /OBJR (object reference) entries, and map annotations back to + * their structure elements through the /ParentTree number tree (keyed by the + * annotation's /StructParent). + * + * When annotations are removed (e.g. widgets during form flattening), these + * references must be cleaned up too. Otherwise: + * - The /OBJR entries keep the removed annotations reachable, so full-save + * garbage collection writes the orphaned objects back into the output. + * - The /ParentTree retains stale keys pointing at structure elements whose + * annotations no longer exist. + * + * PDF Reference: Section 14.7 "Logical Structure" + */ + +import { PdfArray } from "#src/objects/pdf-array"; +import { PdfDict } from "#src/objects/pdf-dict"; +import { PdfNumber } from "#src/objects/pdf-number"; +import type { PdfObject } from "#src/objects/pdf-object"; +import { PdfRef } from "#src/objects/pdf-ref"; + +import type { ObjectRegistry } from "./object-registry"; + +/** + * Remove all structure tree references to the given annotations. + * + * Walks the structure tree from the catalog's /StructTreeRoot and: + * - Removes /OBJR kids whose /Obj points to a removed annotation + * - Removes /ParentTree entries keyed by the removed annotations' + * /StructParent values + * + * No-op if the document has no structure tree. + * + * @param catalog The document catalog dictionary + * @param registry The object registry for resolving references + * @param removedAnnotations Refs of annotations being removed. PdfRefs are + * interned, so identity comparison via Set membership is safe. + */ +export function removeAnnotationsFromStructTree( + catalog: PdfDict, + registry: ObjectRegistry, + removedAnnotations: ReadonlySet, +): void { + if (removedAnnotations.size === 0) { + return; + } + + const resolve = registry.resolve.bind(registry); + const structTreeRoot = catalog.getDict("StructTreeRoot", resolve); + + if (!structTreeRoot) { + return; + } + + // Collect the /StructParent keys of the removed annotations before any + // teardown, so we can prune the matching ParentTree entries. + const removedKeys = new Set(); + + for (const ref of removedAnnotations) { + const annot = resolve(ref); + + if (annot instanceof PdfDict) { + const structParent = annot.getNumber("StructParent")?.value; + + if (structParent !== undefined) { + removedKeys.add(structParent); + } + } + } + + pruneObjrKids(structTreeRoot, registry, removedAnnotations); + + if (removedKeys.size > 0) { + const parentTree = structTreeRoot.getDict("ParentTree", resolve); + + if (parentTree) { + pruneNumberTree(parentTree, registry, removedKeys, new Set()); + } + } +} + +/** + * Check if a dict is an /OBJR entry pointing at one of the removed annotations. + */ +function isRemovedObjr(dict: PdfDict, removed: ReadonlySet): boolean { + if (dict.getName("Type")?.value !== "OBJR") { + return false; + } + + const obj = dict.get("Obj"); + + return obj instanceof PdfRef && removed.has(obj); +} + +/** + * Check if a dict is a structure element (as opposed to an /MCR or /OBJR kid). + * Structure elements may omit /Type, in which case /StructElem is assumed. + */ +function isStructElement(dict: PdfDict): boolean { + const type = dict.getName("Type")?.value; + + return type === undefined || type === "StructElem"; +} + +/** + * Walk the structure tree and remove /OBJR kids referencing removed annotations. + * + * The /K entry of a structure element can be: a number (MCID), a dict + * (struct element, MCR, or OBJR), a ref to either, or an array of any of + * these. All forms are handled. + */ +function pruneObjrKids( + root: PdfDict, + registry: ObjectRegistry, + removed: ReadonlySet, +): void { + const resolve = registry.resolve.bind(registry); + const visited = new Set(); + const stack: PdfDict[] = [root]; + + while (stack.length > 0) { + const elem = stack.pop()!; + + if (visited.has(elem)) { + continue; + } + + visited.add(elem); + + const k = elem.get("K"); + const kResolved = k instanceof PdfRef ? resolve(k) : k; + + if (kResolved instanceof PdfArray) { + // Filter in place (iterate backwards so removal doesn't shift indices) + for (let i = kResolved.length - 1; i >= 0; i--) { + const item = kResolved.at(i); + const itemResolved = item instanceof PdfRef ? resolve(item) : item; + + if (!(itemResolved instanceof PdfDict)) { + continue; // MCID numbers etc. + } + + if (isRemovedObjr(itemResolved, removed)) { + kResolved.remove(i); + } else if (isStructElement(itemResolved)) { + stack.push(itemResolved); + } + } + + if (kResolved.length === 0) { + elem.delete("K"); + } + } else if (kResolved instanceof PdfDict) { + if (isRemovedObjr(kResolved, removed)) { + elem.delete("K"); + } else if (isStructElement(kResolved)) { + stack.push(kResolved); + } + } + } +} + +/** + * Remove entries with the given keys from a number tree (the /ParentTree). + * + * Handles both flat trees (/Nums on the root) and trees with intermediate + * /Kids nodes. Recomputes /Limits on modified leaf nodes. + */ +function pruneNumberTree( + node: PdfDict, + registry: ObjectRegistry, + keys: ReadonlySet, + visited: Set, +): void { + if (visited.has(node)) { + return; + } + + visited.add(node); + + const resolve = registry.resolve.bind(registry); + const kids = node.getArray("Kids", resolve); + + if (kids) { + for (let i = 0; i < kids.length; i++) { + const kid = kids.at(i, resolve); + + if (kid instanceof PdfDict) { + pruneNumberTree(kid, registry, keys, visited); + } + } + } + + const nums = node.getArray("Nums", resolve); + + if (!nums) { + return; + } + + // Nums is a flat [key value key value ...] array + const remaining: PdfObject[] = []; + const remainingKeys: number[] = []; + let changed = false; + + for (let i = 0; i + 1 < nums.length; i += 2) { + const keyObj = nums.at(i, resolve); + const key = keyObj instanceof PdfNumber ? keyObj.value : undefined; + + if (key !== undefined && keys.has(key)) { + changed = true; + continue; + } + + remaining.push(nums.at(i)!, nums.at(i + 1)!); + + if (key !== undefined) { + remainingKeys.push(key); + } + } + + if (!changed) { + return; + } + + node.set("Nums", new PdfArray(remaining)); + + // Keep /Limits consistent with the remaining keys (required on all nodes + // except the root; harmless to update wherever it exists). + if (node.has("Limits")) { + if (remainingKeys.length > 0) { + node.set( + "Limits", + PdfArray.of( + new PdfNumber(Math.min(...remainingKeys)), + new PdfNumber(Math.max(...remainingKeys)), + ), + ); + } else { + node.delete("Limits"); + } + } +} From 1c8a52b380e833e4fe47e1a81736f33878d0c8d7 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 12 Jun 2026 14:34:44 +1000 Subject: [PATCH 3/3] chore: bump dependencies --- bun.lock | 110 +++++++++++++++++++++++++++------------------------ package.json | 14 +++---- 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/bun.lock b/bun.lock index e8ebb8d..15366b6 100644 --- a/bun.lock +++ b/bun.lock @@ -41,11 +41,11 @@ }, }, "packages": { - "@babel/generator": ["@babel/generator@8.0.0-rc.5", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.5", "@babel/types": "^8.0.0-rc.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg=="], + "@babel/generator": ["@babel/generator@8.0.0-rc.6", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.6", "@babel/types": "^8.0.0-rc.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.5", "", {}, "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.6", "", {}, "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], @@ -53,13 +53,13 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@cantoo/pdf-lib": ["@cantoo/pdf-lib@2.6.5", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "crypto-js": "^4.2.0", "node-html-better-parser": ">=1.4.0", "pako": "^1.0.11", "tslib": ">=2" } }, "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA=="], + "@cantoo/pdf-lib": ["@cantoo/pdf-lib@2.7.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "color": "^4.2.3", "crypto-js": "^4.2.0", "node-html-better-parser": ">=1.4.0", "pako": "^1.0.11", "tslib": ">=2" } }, "sha512-oHVfp0JrHYodyF18dG1r85LStjJJs42LLoiu+elB9qZu9YfYoE66omF5hJ3gEKEXk6PPUUEDpBngeZiHMSnueQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -113,9 +113,9 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "@google-cloud/kms": ["@google-cloud/kms@5.5.0", "", { "dependencies": { "google-gax": "^5.0.0" } }, "sha512-feaZLZ0G64gxejaemMUSliBkbzEicgXP+AFxt8DRUjRBTbP8qpHlZcGQxC+J+YF1pg0sTcV+WNy2syGW23irpA=="], + "@google-cloud/kms": ["@google-cloud/kms@5.5.1", "", { "dependencies": { "google-gax": "^5.0.0" } }, "sha512-7O3mnspIlotzToIsSrv+YfnGhGdncOyZmjI0gG/r+jcsYN0t8WCa8v9YGuZ0F1VRa+49p/hUT/6ZQh9/s79PfA=="], - "@google-cloud/secret-manager": ["@google-cloud/secret-manager@6.1.2", "", { "dependencies": { "google-gax": "^5.0.0" } }, "sha512-X4GiHC1OsZU8LOcnM/hk7Q+W/orZLSJz6IAvBJrjaQCj1pSRHkTHJnU87A6E5G8/ubIJjL6vs1GJ8f17TkDzPw=="], + "@google-cloud/secret-manager": ["@google-cloud/secret-manager@6.1.3", "", { "dependencies": { "google-gax": "^5.0.0" } }, "sha512-3e/5GLusy1sWBEUvIlJbJpaaUlaf4MeeGQDUBdIVCqWYnn0lNPtbO6ZbJhTPiOkct2yxi8DXWgzWDbCJdDq9Bw=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], @@ -133,13 +133,13 @@ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - "@oxc-project/types": ["@oxc-project/types@0.130.0", "", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="], + "@oxc-project/types": ["@oxc-project/types@0.135.0", "", {}, "sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-ICXQVKrDvsWUtfx6EiVJxfWrajKTwTfRV8vz2XiMkxZeuCKJLgD4YAj6dE3BWvpqDlkVkie4VSTAtMUWO9LDXg=="], @@ -235,35 +235,35 @@ "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.1", "", { "os": "android", "cpu": "arm64" }, "sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.1", "", { "os": "none", "cpu": "arm64" }, "sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.1", "", { "dependencies": { "@emnapi/core": "1.11.0", "@emnapi/runtime": "1.11.0", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -317,7 +317,7 @@ "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], @@ -335,17 +335,17 @@ "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="], - "@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="], + "@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="], - "@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], - "@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="], + "@vitest/runner": ["@vitest/runner@4.1.8", "", { "dependencies": { "@vitest/utils": "4.1.8", "pathe": "^2.0.3" } }, "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ=="], - "@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="], + "@vitest/spy": ["@vitest/spy@4.1.8", "", {}, "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA=="], "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], @@ -357,7 +357,7 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + "ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="], "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], @@ -427,7 +427,7 @@ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -531,7 +531,7 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "lru-cache": ["lru-cache@11.4.0", "", {}, "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -613,15 +613,15 @@ "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - "rolldown": ["rolldown@1.0.1", "", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="], + "rolldown": ["rolldown@1.1.1", "", { "dependencies": { "@oxc-project/types": "=0.135.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.1", "@rolldown/binding-darwin-arm64": "1.1.1", "@rolldown/binding-darwin-x64": "1.1.1", "@rolldown/binding-freebsd-x64": "1.1.1", "@rolldown/binding-linux-arm-gnueabihf": "1.1.1", "@rolldown/binding-linux-arm64-gnu": "1.1.1", "@rolldown/binding-linux-arm64-musl": "1.1.1", "@rolldown/binding-linux-ppc64-gnu": "1.1.1", "@rolldown/binding-linux-s390x-gnu": "1.1.1", "@rolldown/binding-linux-x64-gnu": "1.1.1", "@rolldown/binding-linux-x64-musl": "1.1.1", "@rolldown/binding-openharmony-arm64": "1.1.1", "@rolldown/binding-wasm32-wasi": "1.1.1", "@rolldown/binding-win32-arm64-msvc": "1.1.1", "@rolldown/binding-win32-x64-msvc": "1.1.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg=="], - "rolldown-plugin-dts": ["rolldown-plugin-dts@0.25.1", "", { "dependencies": { "@babel/generator": "8.0.0-rc.5", "@babel/helper-validator-identifier": "8.0.0-rc.5", "@babel/parser": "8.0.0-rc.4", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20260325.1", "rolldown": "^1.0.0", "typescript": "^5.0.0 || ^6.0.0", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-zK82aC/8z1iVW+g0bCnlQZq04Y5bNeL/RcRwTYBwsnU6wH0N+6vpIFkN7JC0kYRS5qKA+pxQyfIPvXJ6Q5xSpQ=="], + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.25.2", "", { "dependencies": { "@babel/generator": "8.0.0-rc.6", "@babel/helper-validator-identifier": "8.0.0-rc.6", "@babel/parser": "8.0.0-rc.6", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20260325.1", "rolldown": "^1.0.0", "typescript": "^5.0.0 || ^6.0.0", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg=="], "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -667,7 +667,7 @@ "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], @@ -675,7 +675,7 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "tsdown": ["tsdown@0.22.0", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.0", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.1", "picomatch": "^4.0.4", "rolldown": "^1.0.0", "rolldown-plugin-dts": "^0.25.0", "semver": "^7.7.4", "tinyexec": "^1.1.2", "tinyglobby": "^0.2.16", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.0", "@tsdown/exe": "0.22.0", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-FgW0hHb27nGQA/+F3d5+U9wKXkfilk9DVkc5+7x/ZqF03g+Hoz/eeApT32jqxATt9eRoR+1jxk7MUMON+O4CXw=="], + "tsdown": ["tsdown@0.22.2", "", { "dependencies": { "ansis": "^4.3.1", "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.1", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.1", "picomatch": "^4.0.4", "rolldown": "~1.1.0", "rolldown-plugin-dts": "^0.25.2", "semver": "^7.8.1", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.2", "@tsdown/exe": "0.22.2", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-VX9gsyKXsTnBZjnIM4jsHl9aRv+GfgkE/k1hQslilaBfZMlaw3JuGR+6yhiU0QxWBtOCDnTjwOSoXzgB7Rr50g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -689,7 +689,7 @@ "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], - "vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="], + "vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -711,9 +711,9 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@babel/generator/@babel/parser": ["@babel/parser@8.0.0-rc.5", "", { "dependencies": { "@babel/types": "^8.0.0-rc.5" }, "bin": "./bin/babel-parser.js" }, "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ=="], + "@babel/generator/@babel/parser": ["@babel/parser@8.0.0-rc.6", "", { "dependencies": { "@babel/types": "^8.0.0-rc.6" }, "bin": "./bin/babel-parser.js" }, "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog=="], - "@babel/generator/@babel/types": ["@babel/types@8.0.0-rc.5", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.5", "@babel/helper-validator-identifier": "^8.0.0-rc.5" } }, "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA=="], + "@babel/generator/@babel/types": ["@babel/types@8.0.0-rc.6", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.6", "@babel/helper-validator-identifier": "^8.0.0-rc.6" } }, "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw=="], "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -727,15 +727,15 @@ "@pdf-lib/upng/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/expect/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "@vitest/expect/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], "@vitest/pretty-format/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "@vitest/runner/@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/runner/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], - "@vitest/snapshot/@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "@vitest/snapshot/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], @@ -759,7 +759,7 @@ "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - "rolldown-plugin-dts/@babel/parser": ["@babel/parser@8.0.0-rc.4", "", { "dependencies": { "@babel/types": "^8.0.0-rc.4" }, "bin": "./bin/babel-parser.js" }, "sha512-0S/1yefMa15N4i2v3t8Fw9pgMHhf2gF6Lc1UEXI96Ls6FNAjqvHHZouZ2ZS/deqLhbMFtmfVeFac6iTsvFbLwA=="], + "rolldown-plugin-dts/@babel/parser": ["@babel/parser@8.0.0-rc.6", "", { "dependencies": { "@babel/types": "^8.0.0-rc.6" }, "bin": "./bin/babel-parser.js" }, "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -775,13 +775,17 @@ "tsdown/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "tsdown/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "vitest/@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + "vitest/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], + + "vitest/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "vitest/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "vitest/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], @@ -795,7 +799,7 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.5", "", {}, "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A=="], + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.6", "", {}, "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -813,7 +817,7 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "rolldown-plugin-dts/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.5", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.5", "@babel/helper-validator-identifier": "^8.0.0-rc.5" } }, "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA=="], + "rolldown-plugin-dts/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.6", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.6", "@babel/helper-validator-identifier": "^8.0.0-rc.6" } }, "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -833,7 +837,9 @@ "ast-kit/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.5", "", {}, "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A=="], - "rolldown-plugin-dts/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.5", "", {}, "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A=="], + "ast-kit/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.5", "", {}, "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg=="], + + "rolldown-plugin-dts/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.6", "", {}, "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } diff --git a/package.json b/package.json index d9d14a5..cc4f9e9 100644 --- a/package.json +++ b/package.json @@ -68,14 +68,14 @@ "@noble/hashes": "^2.2.0", "@scure/base": "^2.2.0", "asn1js": "^3.0.10", - "lru-cache": "^11.4.0", + "lru-cache": "^11.5.1", "pako": "^2.1.0", "pkijs": "^3.4.0" }, "devDependencies": { - "@cantoo/pdf-lib": "^2.6.5", - "@google-cloud/kms": "^5.5.0", - "@google-cloud/secret-manager": "^6.1.2", + "@cantoo/pdf-lib": "^2.7.1", + "@google-cloud/kms": "^5.5.1", + "@google-cloud/secret-manager": "^6.1.3", "@types/bun": "^1.3.14", "@types/pako": "^2.0.4", "@vitest/coverage-v8": "4.0.16", @@ -83,11 +83,11 @@ "lint-staged": "^16.4.0", "oxfmt": "^0.50.0", "oxlint": "~1.39.0", - "oxlint-tsgolint": "^0.11.1", + "oxlint-tsgolint": "^0.11.5", "pdf-lib": "^1.17.1", - "tsdown": "^0.22.0", + "tsdown": "^0.22.2", "typescript": "^5.9.3", - "vitest": "^4.1.6" + "vitest": "^4.1.8" }, "peerDependencies": { "@google-cloud/kms": "^5.0.0",