Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
76ffe5b
Start new v0.12.0 development
Aug 29, 2026
b49d9b7
feat(pdf): render invoices & UPO to PDF via optional ksef-client-ts/p…
Jul 5, 2026
f6f3849
fix(pdf): render bank account details & fix NaN QR verification date
Jul 5, 2026
58d1b91
ci(release): run package-export guards on the release path
Aug 17, 2026
c02eeb4
fix(cli): register `invoice pdf` in the shell completion tree
Aug 29, 2026
b7f1235
fix(pdf): total net and VAT rows only counted the standard-rate bucket
Aug 29, 2026
a34e869
fix(pdf): session UPO receipts dropped every document after the first
Aug 29, 2026
5179404
fix(pdf): an unrecognized document passed the template schema check
Aug 29, 2026
e5e6ba0
fix(cli): invoice pdf ignored --template for UPO input
Aug 29, 2026
e1e992f
test(pdf): lint `when` and repeater paths in the built-in templates
Aug 29, 2026
2deaab2
fix(pdf): tables ran off the page edge on real invoices
Aug 29, 2026
c2a3676
feat(pdf): print the KSeF number in the invoice header
Aug 29, 2026
d905b2e
feat(pdf): rework the invoice layout and cover it with a rendering E2E
Aug 29, 2026
82fa766
test(pdf): cover the library surface the CLI cannot reach
Aug 29, 2026
5511b5a
test(pdf): render both preview sets into one directory, grouped by pr…
Aug 29, 2026
75e4eab
feat(pdf): let the reader choose which totals a visualization shows
Aug 29, 2026
6b191ea
fix(pdf): make strict mode usable, and keep it policing the amount due
Aug 29, 2026
2c03f6f
feat(pdf): frame the payment block and move the credit into a page fo…
Aug 29, 2026
1bb21d3
feat(pdf): give the party panel a style vocabulary, move the OFFLINE …
Aug 29, 2026
c40529a
feat(pdf): print both KSeF verification codes, sized to a real scanner
Aug 29, 2026
bcc6608
feat(pdf): add Ukrainian labels, and cover the preview set by design
Aug 29, 2026
9f601de
feat(pdf): let a template style block headings, on two levels
Aug 29, 2026
919a81d
feat(pdf): let the caller add sections of their own to a visualization
Aug 29, 2026
dc642c3
feat(pdf): add fa3-showcase, a built-in that exercises the template DSL
Aug 29, 2026
78a71c9
fix(pdf): let a failed render reject instead of killing the process
Aug 29, 2026
7962006
fix(pdf): stop accepting logo formats the renderer cannot draw
Aug 29, 2026
f17046d
feat(pdf): name the currency an invoice is settled in
Aug 29, 2026
0b345ef
fix(pdf): print amounts as written, not as a double can hold them
Aug 29, 2026
875c2f6
fix(pdf): make the accent colour actually paint something
Aug 29, 2026
c0a36fb
fix(pdf): honour the optional marker in an annotations block
Aug 29, 2026
5647c58
feat(pdf): set the accent colour from the command line
Aug 29, 2026
08fdd8a
feat(pdf): set the accent colour from the command line
Aug 29, 2026
cdf9ad3
feat(pdf): expose the built-in templates, and make the preview set ea…
Aug 29, 2026
cbf3a7a
feat(pdf): print the line-item classifiers under the item they describe
Aug 29, 2026
24f1b1a
feat(pdf): restate the amount due, with its currency, under the payme…
Aug 29, 2026
9f7b830
feat(pdf): head the notes section, and put the notes a level below it
Aug 29, 2026
5f6a50c
feat(pdf): let a template emphasise one totals row, and stop emphasis…
Aug 29, 2026
d196e4b
fix(cli): accept a note that carries only one of its two halves
Aug 29, 2026
8331dfc
fix(pdf): reject a pdfmake prerelease the supported range excludes
Aug 29, 2026
4d453db
fix(pdf): make the version detectors read the markers they claim to
Aug 29, 2026
602663a
fix(pdf): refuse to build a verification code with no seller NIP
Aug 29, 2026
1ce11bd
fix(pdf): draw no table rather than an empty one
Aug 29, 2026
da85768
fix(qr): reject an issue date that is not a real calendar day
Aug 29, 2026
4c64f71
test(pdf): cut the preview set to the pages a reader learns something…
Aug 29, 2026
ab031ee
fix(pdf): anchor the UPO version scan to the document's first element
Aug 29, 2026
24aa604
docs(pdf): say what a notes block's headingStyle actually styles
Aug 29, 2026
d8cae76
fix(qr): catch an impossible issue date in any form it arrives in
Aug 29, 2026
bead30c
fix(pdf): print the order rows an advance invoice carries
Aug 29, 2026
b74ab95
fix(cli): refuse a PDF environment that has no QR host
Aug 29, 2026
1ab31b0
fix(pdf): draw a rule across the page it is actually on
Aug 29, 2026
61e2475
fix(pdf): name P_15 for what it is on this document
Aug 30, 2026
61d0d86
fix(pdf): print the buyer's identifier with the country it belongs to
Aug 30, 2026
6720af1
fix(pdf): let a strict render accept an invoice with no buyer name
Aug 30, 2026
d52777d
fix(pdf): read the buyer address through the element that may be absent
Aug 30, 2026
eeaa228
fix(pdf): print every payment term, not just the first
Aug 30, 2026
2d21598
fix(pdf): read the UPO version from the namespace the root is bound to
Aug 30, 2026
42ce6f8
feat(pdf): show what an invoice settled in instalments has paid
Aug 30, 2026
b806305
feat(pdf): render the whole advance-and-settlement story
Aug 30, 2026
e87aa55
fix(pdf): tax a settlement invoice on the remainder, not the whole order
Aug 30, 2026
7649f91
feat(pdf): bridge the order to the remainder on a settlement invoice
Aug 30, 2026
f948e63
test(pdf): render the second chain in English
Aug 30, 2026
e4e5acb
fix(pdf): find the UPO root tag by scanning, not by pattern
Aug 30, 2026
273cb67
refactor(pdf): make a computed figure's shape a type, not a convention
Aug 30, 2026
87bb4e3
fix(pdf): refuse a payment row that is both read and computed
Aug 30, 2026
844264f
refactor(pdf): make a payment row's two shapes a type, not a convention
Aug 30, 2026
49955fd
fix(pdf): pay instalments against the payable, not against P_15
Aug 30, 2026
5e04982
fix(errors): keep one catch-all working across the package's entry po…
Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/bump-version/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ Prepare a new version release: create a version branch, bump version in package.
**Important**

- Do NOT push to remote.
- The branch name MUST use `version/` prefix per project convention (see CLAUDE.md).
- The branch name MUST use `version/` prefix per project convention (see AGENTS.md).
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ knowledge_base:
code_guidelines:
enabled: true
filePatterns:
- "CLAUDE.md"
- "AGENTS.md"
learnings:
scope: "auto"

Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ jobs:
- name: Check /node entry types
run: yarn check:node-types

# Proves the ./pdf public types resolve WITHOUT @types/pdfmake in scope —
# a consumer that has not installed pdfmake still type-checks ./pdf.
- name: Check /pdf entry types
run: yarn check:pdf-types

# Runs on every matrix Node (18/20/22): the ./pdf subpath imports/requires
# without eagerly loading pdfmake and never throws at module-load time.
- name: Check /pdf cold subpath
run: node packages/ksef-client-ts/scripts/check-pdf-cold.mjs

# Publish-correctness of the package exports (incl. the new ./pdf subpath).
# Runs once — the built dist is identical across the matrix.
- name: Check package exports (attw + publint)
if: matrix.node-version == 22
run: |
yarn check:attw
yarn check:publint

- name: Check fs-free core bundles
run: node packages/ksef-client-ts/scripts/check-fs-free-core.mjs

Expand Down
30 changes: 24 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@ jobs:
run: yarn install --immutable

- name: Validate build
# Build first: the fs-free guard test and the /node type check both
# read the built dist, so the bundle must exist before lint/test run.
run: yarn build && yarn lint && yarn check:node-types && yarn test
# Build first: every guard below reads the built dist. Keep this list in
# sync with the guards in ci.yml: a tag can point at a commit that never
# went through PR CI, so this is the only gate the published artifact hits.
run: |
yarn build
yarn lint
yarn check:node-types
yarn check:pdf-types
node packages/ksef-client-ts/scripts/check-pdf-cold.mjs
yarn check:attw
yarn check:publint
yarn test

- name: Publish package
working-directory: packages/ksef-client-ts
Expand Down Expand Up @@ -141,9 +150,18 @@ jobs:
run: yarn install --immutable

- name: Validate build
# Build first: the fs-free guard test and the /node type check both
# read the built dist, so the bundle must exist before lint/test run.
run: yarn build && yarn lint && yarn check:node-types && yarn test
# Build first: every guard below reads the built dist. Keep this list in
# sync with the guards in ci.yml: a tag can point at a commit that never
# went through PR CI, so this is the only gate the published artifact hits.
run: |
yarn build
yarn lint
yarn check:node-types
yarn check:pdf-types
node packages/ksef-client-ts/scripts/check-pdf-cold.mjs
yarn check:attw
yarn check:publint
yarn test

- name: Prepare scoped package name
working-directory: packages/ksef-client-ts
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ packages/ksef-client-ts/docs/public/open-api.json
# markdown-vault MCP cache
packages/ksef-client-ts/docs/.markdown_vault_mcp/

# Rendered PDF previews from the `invoice pdf` E2E spec — reviewed by eye, never committed
packages/ksef-client-ts/.pdf-preview/

# macOS
.DS_Store

Expand Down
196 changes: 196 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# AGENTS.md

Guidance for any AI coding agent working in this repository: how the project is laid out, which commands to run, and the conventions a change is expected to follow. Read it before making changes — it takes precedence over habits carried in from other repositories.

## Project

Yarn 4.x workspace monorepo. The library (`ksef-client-ts`) lives in `packages/ksef-client-ts/`. TypeScript client for the Polish National e-Invoice System (KSeF) API v2. Targets Node.js 18+ with dual ESM/CJS output. Current version and release history are in `packages/ksef-client-ts/CHANGELOG.md`.

## Commands

Run from the **repo root** — all commands delegate to the `ksef-client-ts` workspace:

```bash
yarn build # Build ESM + CJS + DTS via tsup
yarn lint # Type-check only (tsc --noEmit)
yarn test # Run unit tests (vitest run tests/unit)
yarn test:e2e # Run E2E tests (vitest run tests/e2e)
yarn test:watch # Watch mode (all tests)
yarn docs:dev # VitePress dev server
yarn docs:build # Build docs site
yarn check-api # Check OpenAPI coverage
yarn sync-openapi # Download the OpenAPI spec from the live KSeF API
yarn split-openapi # Split open-api.json into per-domain chunks
yarn sync-schemas # Download XSD schemas from CIRFMF/ksef-docs
```

Run a single test file: `yarn workspace ksef-client-ts vitest run tests/unit/foo.test.ts`

Tests live in `packages/ksef-client-ts/tests/**/*.test.ts` (vitest, globals enabled). Unit tests in `tests/unit/`, E2E tests in `tests/e2e/` (relative to the package).

E2E specs drive the **live KSeF TEST API** (`environment: 'TEST'`, creds from `KSEF_TEST_TOKEN`/`KSEF_TEST_NIP`) — never DEMO or PROD. The PDF specs (35, 36) are the exception: no network at all, they render locally into `.pdf-preview/` (override with `KSEF_PDF_OUT`) and need `yarn build` first; `--env test` there only picks the host printed in the QR link, so keep it on TEST like everything else.

**Package manager is yarn 4.x** (Corepack). Do not use npm. The `.yarnrc.yml` sets `nodeLinker: node-modules`.

## Architecture

### Layered design

Source paths below are relative to the library package, `packages/ksef-client-ts/`.

```text
KSeFClient (src/client.ts)
├── 14 API services + crypto + qr + offline (17 properties total)
├── each service wraps RestClient for its API domain
├── crypto is lazy-initialized (user calls client.crypto.init())
└── offline is lazy-initialized (accessed via client.offline)

Services (src/services/*.ts) — 14 services
└── use RestClient.execute<T>() with RestRequest builders + Routes constants

HTTP layer (src/http/)
├── RestClient — wraps native fetch, handles errors (429/401/403), JSON, auth headers
├── RestRequest — fluent builder (method, path, body, headers, query)
├── RouteBuilder — prepends /v2/ version prefix
├── Routes — all API endpoint paths as const object
├── RetryPolicy — exponential backoff with jitter, configurable retryable status codes
├── RateLimitPolicy — token bucket rate limiter (global + per-endpoint)
├── CircuitBreakerPolicy — opt-in fail-fast above retry: opens after N consecutive network/5xx failures, probes after cooldown (429/401 never trip)
├── PresignedUrlPolicy — validates presigned download URLs (HTTPS, host allowlist)
└── AuthManager — manages access/refresh tokens, auto-refresh on 401 with dedup

Crypto layer (src/crypto/)
├── CertificateFetcher — fetches & caches KSeF public certs
├── CryptographyService — AES-256-CBC, RSA-OAEP, ECDH+AES-GCM, CSR gen
├── SignatureService — XAdES-B enveloped XML signatures (static)
└── CertificateService — self-signed cert generation (static)

QR layer (src/qr/)
├── VerificationLinkService — builds invoice/certificate verification URLs
└── QrCodeService — generates QR codes (PNG, SVG, SVG+label)

Offline layer (src/offline/)
├── types — OfflineMode, OfflineInvoiceStatus, OfflineInvoiceMetadata, OfflineCertificate
├── deadline — calculateOfflineDeadline(), business day helpers, maintenance cascading
├── storage — OfflineInvoiceStorage interface + InMemoryOfflineInvoiceStorage
└── file-storage — FileOfflineInvoiceStorage (~/.ksef/offline/)

XML layer (src/xml/)
├── upo-parser — parses official KSeF UPO receipt XML into typed objects
├── invoice-field-extractor — extracts P_1/P_2/P_4B/P_4C from invoice XML
├── xml-engine — fast-xml-parser wrapper (preserveOrder); parseXml/buildXml/stripBom
├── order-map — ORDER_MAP per XSD parent + comparePKey natural sort +
│ multi-rate P_13/P_14/P_14W interleave per VAT group
├── faktura-builder — FA2/FA3 builder; injects xmlns + xmlns:etd on <Faktura>
├── pef-builder — PEF (Invoice) / PEF_KOR (CreditNote) UBL builder
└── invoice-serializer — polymorphic serializeInvoiceXml(input, options) → Buffer
dispatching on FakturaInput / PefUblDocumentInput / string / Buffer / XmlDocument

CLI (src/cli/) — 17 command groups via citty
├── setup, auth, session, invoice, permission, token, cert, lighthouse, limits,
│ collective-identifier, peppol, test-data, qr, config, doctor, completion,
│ offline
├── requireSession() — auto-recovers via refresh or re-login from stored credentials
└── session-recovery — cascade: refresh token → loginWithToken from credentials → error
```

### Key conventions

- **Imports use `.js` extensions** (ESM resolution convention, even for `.ts` source files).
- **Models** are in `src/models/{domain}/types.ts` with barrel `index.ts` re-exports. Types from `src/models/common.ts` are shared across domains.
- **Builders** in `src/builders/` provide fluent APIs for complex request construction.
- **Static vs instance**: `SignatureService` and `CertificateService` are fully static (no state). `CryptographyService` requires a `CertificateFetcher` instance (injected via `KSeFClient` constructor).
- **No auto-init**: `CryptographyService.init()` must be called explicitly to fetch KSeF public certificates. It is NOT called in the `KSeFClient` constructor.

### Naming collisions to be aware of

- `CertificateApiService` (src/services/) — API CRUD for certificate enrollment. Named with "Api" suffix to avoid collision with `CertificateService` (src/crypto/) which handles self-signed cert generation.
- `InvoiceFilterInvoicingMode` (not `InvoicingMode`) — avoids collision with session types.
- `PermissionSubjectIdentifierType` (not `SubjectIdentifierType`) — avoids collision with auth types. Note: both now use `'Nip' | 'Pesel' | 'Fingerprint'` values (aligned with OpenAPI spec).

### KSeF environments and portals

| Env | API | Web Portal |
|-----|-----|------------|
| PROD | `https://api.ksef.mf.gov.pl` | `https://ap.ksef.mf.gov.pl/web/` |
| TEST | `https://api-test.ksef.mf.gov.pl` | `https://ap-test.ksef.mf.gov.pl/web/` |
| DEMO | `https://api-demo.ksef.mf.gov.pl` | `https://ap-demo.ksef.mf.gov.pl/web/` |

The web portal is used for token generation, permission management, and invoice browsing via browser (requires qualified signature or trusted profile). Each environment is fully isolated — accounts, tokens, and certificates created in one env do not exist in others.

### Environment variables

`KSEF_NIP`, `KSEF_TOKEN` (for PROD) and `KSEF_TOKEN_DEMO` (for DEMO) are set in the current shell environment.

- PROD: `ksef auth login --token "$KSEF_TOKEN" --nip "$KSEF_NIP" --env prod`
- DEMO: `ksef auth login --token "$KSEF_TOKEN_DEMO" --nip "$KSEF_NIP" --env demo`

### Invoice upload flow (CLI)

```bash
ksef auth login --token "$KSEF_TOKEN" --nip "$KSEF_NIP"
ksef session open # 1. Open online session (required before sending)
ksef invoice build data.json # (optional) Build XML from JSON/YAML; `--template FA3` prints a skeleton.
ksef invoice send file.xml # 2. Send invoice
ksef session invoices # 3. Verify invoice status (check for errors/duplicates)
ksef invoice query --from 2026-01-01 # Query invoices by date range
ksef session close # 4. Close session (optional)
```

Invoice number (`P_2` in XML) must be unique — resubmitting gives error 440 (Duplikat faktury).

### OpenAPI spec

`packages/ksef-client-ts/docs/open-api.json` is the KSeF API OpenAPI specification (source of truth, KSeF API v2.7.1, build `2.7.1-te`; synced from the live TEST endpoint `https://api-test.ksef.mf.gov.pl/docs/v2/openapi.json`). Note: TEST/DEMO lead while PROD trails, so the vendored spec can be ahead of what PROD serves. Update it with `yarn sync-openapi` (`--env demo|prod` to pull from another environment, `--dry-run` to preview the delta), which writes the served document verbatim. Per-domain chunks in `packages/ksef-client-ts/docs/openapi-chunks/` (10 chunks + manifest; descriptions stripped to save tokens). Regenerate with `yarn split-openapi` after every sync. Validate coverage with `yarn check-api`.

### XSD schemas

`packages/ksef-client-ts/docs/schemas/` contains official KSeF invoice XSD schemas from [CIRFMF/ksef-docs](https://github.com/CIRFMF/ksef-docs). Organized by type: `FA/` (standard invoices), `PEF/` (Peppol), `RR/` (farmer invoices), each with `bazowe/` base types. Update with `yarn sync-schemas`.

### Error hierarchy

`KSeFError` (base) → `KSeFApiError` (generic HTTP), `KSeFBadRequestError` (400), `KSeFUnauthorizedError` (401), `KSeFForbiddenError` (403), `KSeFGoneError` (410, retention expired), `KSeFRateLimitError` (429), `KSeFBatchTimeoutError` (KSeF code 21208), `KSeFUnknownPublicKeyError` (KSeF code 21470, pre-empts `KSeFBadRequestError`), `KSeFAuthStatusError`, `KSeFSessionExpiredError`, `KSeFValidationError` (builder validation), `KSeFXsdValidationError` (XSD schema validation), `KSeFMetadataPaginationError` (paging cannot advance), `KSeFCircuitOpenError` (circuit breaker fail-fast).

`RestClient.ensureSuccess` reads body text once, then parses per status code (400→429→401→403→410), falling back to a KSeF-error-code check and then generic `KSeFApiError`.

### CI/CD

GitHub Actions workflows in `.github/workflows/` (the `.github/` dir stays at the repo root; build/test steps run root-level `yarn` scripts that delegate to the `ksef-client-ts` workspace):
- `ci.yml` — markdown lint + unit + E2E tests on Node 18/20/22 matrix, coverage badge via gist (coverage JSON read from `packages/ksef-client-ts/coverage/`)
- `release.yml` — on tag `v*`: create GitHub Release (from `packages/ksef-client-ts/CHANGELOG.md`), then publish to npm + GitHub Packages in parallel
- `deploy-docs.yml` — VitePress → GitHub Pages (artifact from `packages/ksef-client-ts/docs/.vitepress/dist`)
- `deno-smoke.yml` — Deno runtime smoke test (`deno task smoke`, run in the package dir)
- `codex-pr-review.yml` — automatic Codex PR review on open/sync (prompt: `.github/codex/prompts/review.md`)

### Documentation

VitePress site in `packages/ksef-client-ts/docs/` with Scalar API reference. Config: `packages/ksef-client-ts/docs/.vitepress/config.ts`.
Feature descriptions live in two places that must be kept in sync: the root `README.md` (bullet list — the canonical repo landing page on GitHub) and `packages/ksef-client-ts/docs/index.md` (VitePress homepage cards). The package `packages/ksef-client-ts/README.md` is intentionally a thin npm-only intro (install + quick start + links) and is NOT a mirror of the feature list — do not duplicate the full feature bullets there.

### Plans

`plans/` directory (gitignored) contains development plans and roadmaps. Not tracked in git.

- `plans/references.md` — reference-project comparison: maturity, crypto, feature gaps, KSeF API changelog
- `plans/shipped.md` — feature history by version
- `plans/backlog.md` — pending work, release proposals, deferred items
- `plans/p<priority>-<id>-<slug>.md` — individual feature sub-plans; each is cross-referenced from `backlog.md`

### Reference implementations

`ref/` directory (gitignored) contains reference implementations, official docs, and related projects. See `ref/ref-index.md` for the full index.

### WebCrypto typing quirk

`crypto.webcrypto.subtle.generateKey()` returns `CryptoKeyPair | CryptoKey`. Cast to `crypto.webcrypto.CryptoKeyPair` when generating key pairs — TypeScript cannot narrow this union.

## Rules

- Do not push commits or create files unless explicitly asked. Do not push to remote unless the user explicitly says "push" — committing and pushing are separate actions. Do not assume the user wants additional actions beyond what was requested.
- When writing CHANGELOG entries, describe the feature's purpose and user-facing impact, NOT implementation details. Keep entries concise — one sentence per bullet, no method names, class names, parameter names, option names, header names, CLI flags, internal field names, or error class identifiers. If an API version reference is useful (e.g. "KSeF API v2.2.0"), keep it in parentheses at the end.
- Always run the full test suite (unit + e2e) before committing. Ensure all tests pass before creating commits.
- When debugging issues, investigate root causes before suggesting surface-level fixes. Don't suggest simple retries or config changes without first checking if the value is hardcoded or the real problem is deeper.
- Always merge PRs with `--squash`. Merge commits are disabled on this repository.
- When squash-merging, edit the combined commit message to remove duplicate `Co-Authored-By` lines from individual commits — keep only a single `Co-Authored-By` at the very end.
- Name version branches with `version/` prefix (e.g. `version/v0.6.1`) to avoid conflicts with release tags.
- When writing documentation (README, docs/**, plans/**, CHANGELOG, etc.), always tag fenced code blocks containing ASCII tables, tree diagrams, or other non-code content with ` ```text ` instead of a bare ` ``` `. Reserve language-less fences only for genuinely untyped snippets.
Loading