npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prozorro/prozorro-pdf-stamp

v0.0.6

Published

Draws a stamp per signer onto a Prozorro econtract PDF

Readme

prozorro-pdf-stamp

Browser library that takes a link to a Prozorro econtract and returns that contract's PDF with one visual stamp per signer drawn on it. Stamp content is read out of the contract's contractSignature documents.

The project is a viewer aid. It does not create a KEP/QES, does not verify one cryptographically, and must never present its output as proof of a valid signature.

Install

npm install @prozorro/prozorro-pdf-stamp

pdf-lib, @pdf-lib/fontkit and @prozorro/prozorro-eds come as dependencies. ESM and CJS builds ship, with TypeScript declarations at dist/index.d.ts.

Browser only. The library uses fetch, Blob, URL.createObjectURL, document and window; it does not run under Node without a DOM.

Quick start

import {
  ProzorroPdfStampService,
  PROZORRO_ENVIRONMENT_MODE,
  PROZORRO_STAMP_TYPES,
  ProzorroPdfStampError,
} from "@prozorro/prozorro-pdf-stamp";

await ProzorroPdfStampService.init(PROZORRO_ENVIRONMENT_MODE.PROD);

const doc = await ProzorroPdfStampService.setConfig({
  type: PROZORRO_STAMP_TYPES.CONTRACT,
  url: "https://public-api.prozorro.gov.ua/api/2.5/contracts/<id>",
});

const result = await doc.validate();

if (result.valid) {
  await doc.open(); // new tab with the stamped PDF
} else {
  result.failures.forEach(failure => console.warn(failure.reason, failure.message));
}

Two steps, always in this order: init() configures the decode service once per page, setConfig() returns one document instance per contract. Calling setConfig() before init() rejects with INVALID_PARAMS / notInitialised before any network request.

API

ProzorroPdfStampService.init(environment, options?)

await ProzorroPdfStampService.init(PROZORRO_ENVIRONMENT_MODE.PROD, {
  fonts: { bold: "https://your-host/tinos-bold.ttf" },
});

| Argument | Type | Meaning | | --------------- | --------------------------------- | -------------------------------------------------------------------------- | | environment | PROZORRO_ENVIRONMENT_MODE | PROD ("production") or DEV ("development") decode service | | options.fonts | { bold?: string \| Uint8Array } | Replaces the bundled stamp face. A string is fetched, bytes are used as-is |

Omit fonts and the bundled Tinos Bold subset is used with no request at all. One face is drawn, so bold is the only field. Calling init twice re-initialises rather than failing.

ProzorroPdfStampService.setConfig(config)

const doc = await ProzorroPdfStampService.setConfig({
  type: PROZORRO_STAMP_TYPES.CONTRACT,
  url: "https://public-api.prozorro.gov.ua/api/2.5/contracts/<id>",
});

type is PROZORRO_STAMP_TYPES.CONTRACT — the only implemented kind. url is the contract's API endpoint, not the web page. Returns a StampDocument; nothing is fetched yet.

StampDocument

| Member | Returns | Does | | ------------------------------------- | ---------------------------- | --------------------------------------------------- | | validate() | Promise<ValidationResult> | Fetches the contract and applies the business rules | | toBytes(config?) | Promise<Uint8Array> | The stamped PDF as bytes | | save(config?, filename?) | Promise<void> | Triggers a browser download | | open(config?) | Promise<void> | Opens the PDF in a new tab | | getIframe(config?, parentElementId) | Promise<HTMLIFrameElement> | Renders into an <iframe> appended to that element | | warnings | readonly Warning[] | The last render's warnings (getter, not a call) |

Every render method takes the same optional placement config:

type StampPlacementConfig = {
  pages?: "first" | "last" | "every"; // default "first"
  placement?: "top" | "bottom"; // default "bottom"
};

pages selects which pages get stamped; placement picks the edge stamps grow from. Stamp size, margins and gap are fixed — the library never shrinks a stamp to fit more signers.

save() without a filename derives one from the contract id and appends .pdf if you did not.

getIframe() needs the id of an element that already exists in the DOM; a missing element is INVALID_PARAMS / incorrectInputFormat.

Each instance caches per contract: the contract JSON, the source PDF, the decoded signers, the font, and one render per distinct (pages, placement) pair. Two concurrent calls with the same config join one run. Nothing is written to localStorage, sessionStorage, IndexedDB, the Cache API or a service worker — state lives on the instance and dies with it, so drop the instance to release the PDF.

Validation

Business-rule failures are returned as data, never thrown:

const { valid, failures } = await doc.validate();

| failure.reason | Contract failed because | | ------------------------------- | ------------------------------------------- | | CONTRACT_STATUS_NOT_ALLOWED | status is not active or terminated | | CONTRACT_TEMPLATE_MISSING | no contractTemplateName field | | CONTRACT_NOTICE_NOT_FOUND | no contractNotice PDF among the documents | | CONTRACT_SIGNATURES_NOT_FOUND | no contractSignature documents |

failure.message is Ukrainian UI copy and may change at any release — branch on reason, never on the string. failure.path names the field the rule read.

validate() is evaluated once per instance. The render methods run it themselves, so an invalid contract makes toBytes() / save() / open() / getIframe() throw VALIDATION_FAILED with the first failure as reason and the whole list under error.context.failures. Calling validate() first is how you show all of them without an exception.

Errors

Everything that is not a business-rule failure throws ProzorroPdfStampError:

try {
  await doc.toBytes({ pages: "every" });
} catch (error) {
  if (error instanceof ProzorroPdfStampError) {
    switch (error.code) {
      case PROZORRO_PDF_STAMP_ERROR_CODES.SERVICE_UNAVAILABLE:
        // retryable
        break;
      default:
        error.logWithTrace();
    }
  }
}

| Field | Use | | --------------- | ------------------------------------------ | | code | Category — decides which UI block you show | | reason | The specific cause — what you branch on | | message | Ukrainian UI copy, provisional | | context | Extra detail (url, failures, …) | | originalError | Whatever was caught underneath | | timestamp | When it was constructed |

| code | reason | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | INVALID_PARAMS | incorrectInputFormat, notInitialised | | VALIDATION_FAILED | contractStatusNotAllowed, contractTemplateMissing, contractNoticeNotFound, contractSignaturesNotFound, documentNotAvailable, noSigners | | SERVICE_UNAVAILABLE | requestFailed, decodeServiceFailed | | INVALID_SIGNATURE | signatureDecodeFailed | | PDF_GENERATION_FAILED | invalidPdf, pdfWriteFailed | | INTERNAL_ERROR | unexpected |

INVALID_SIGNATURE means a signature file could not be read, not that a signature is invalid. A file that failed to download is a transport failure, and an unrecognised fault is INTERNAL_ERROR — never a fake outage.

Errors do not log themselves. Call error.logWithTrace() when you want the console output.

One unreadable signature file fails the whole render: a partial signer list would misrepresent who signed the contract.

Warnings

The second channel. A warning never fails a render — the PDF is still returned — and reports what the stamp had to do:

const bytes = await doc.toBytes();
doc.warnings.forEach(warning => console.info(warning.reason, warning.context));

| warning.reason | Meaning | | -------------------------- | ------------------------------------------------------------- | | DUPLICATE_SIGNER_DROPPED | Two entries shared a certificate serial; one stamp was drawn | | SIGNER_NAME_FALLBACK | No full name in the certificate — the common name was used | | SIGNER_NAME_MISSING | No name at all; the stamp shows a blank area | | SIGNER_CODE_MISSING | No ЄДРПОУ or РНОКПП; the line is omitted | | STAMP_TEXT_TRUNCATED | Text was shrunk, then cut, to stay inside the stamp | | STAMPS_TRUNCATED | Some stamps did not fit; context.page and context.skipped | | PAGE_ROTATION_APPLIED | The page carries a rotation; the stamp was placed accordingly |

doc.warnings holds the most recent render's warnings, deduplicated — reading it before any render gives []. Branch on reason; message is UI copy.

What the stamp shows

Per signer: the fixed heading, the signer's name (subjectFullName, falling back to subjectCN), and the identification code (subjectEDRPOUCode, falling back to subjectDRFOCode). Signers are deduplicated by certificate serial; an entry with no serial is never treated as a duplicate.

The seal is drawn, not embedded — stroked rings plus filled wordmark outlines — so it stays sharp at any zoom and the bundle carries no image. Geometry is in PDF points. Stamps may overlap the contract's own content: the PDF layer is write-only and cannot see what is already on the page.

Byte-for-byte determinism holds — the same contract and config produce the same PDF — and the source document's metadata is left untouched.

Requirements and limits

  • The contract API, the document downloads and the decode service must all be reachable from the browser, with CORS headers that allow your origin. A blocked request surfaces as SERVICE_UNAVAILABLE / requestFailed.
  • Requests carry a 30 s timeout; signature files decode up to four at a time.
  • open() opens a tab — call it from a user gesture or the popup blocker stops it.

Local demo

npm install
npm run dev      # demo page, all API methods wired to a URL field
npm run build    # library + demo bundles
npm run check    # typecheck, lint, tests, gates

Documentation

  • Requirements — FR-01 … FR-24, NFR-01 … NFR-06, ASR, error and warning model, open questions
  • Architecture — layers, module contracts, processing flow, coordinate system, public API
  • Decisions — one file per ADR, indexed
  • Tasks — T-01 … T-22 across eight phases
  • Test Cases — TC-01 … TC-52

Traceability is verified on every npm run check: no dangling references, no gaps in numbering, every FR covered by a task or test, every TC owned by a task, and every ADR either referenced downstream or superseded by a later one.

Stack

TypeScript, Vite, Vitest, ESLint. pdf-lib pinned at 1.17.1 for writing, @prozorro/prozorro-eds for signature decoding, Tinos Bold for Cyrillic stamp text. No pdfjs-dist — the PDF layer is write-only.

Two build artifacts: the npm package @prozorro/prozorro-pdf-stamp, and a single-file demo page.

Status

Implemented. T-01 … T-22 are done, npm run check is green, and the acceptance test takes Fixture A through the public API to a reopened PDF.