Skip to content

Repository files navigation

@finitoapp/eet-client

CI License: MIT

Low-level, portable TypeScript SDK for a single Czech "Elektronická evidence tržeb 2.0" (EET 2.0, electronic sales registration) submission — see Installation for npm/bun/deno. Documentation below is in Czech (the EET protocol itself is Czech-law-specific); see CONTRIBUTING.md if you'd like to contribute.

Nízkoúrovňové, přenositelné TypeScript SDK pro odeslání jedné datové zprávy Elektronické evidence tržeb 2.0 (EET 2.0) podle rozhraní verze 4.1. Funguje v Node.js, Bunu, Denu i moderních prohlížečích. SDK sestaví a podepíše SOAP 1.1 požadavek dle WS-Security/XMLDSig, odešle jej a bezpečně vyhodnotí odpověď systému EET.

Balíček nemá žádné runtime závislosti (zod je jen volitelný peerDependency pro podcestu @finitoapp/eet-client/zod, který se do vašeho bundlu nedostane, pokud ho neimportujete) — celé XML/C14N a XMLDSig zpracování je hand-rolled, viz Validace vstupu.

Neobsahuje žádné doménové workflow pokladny ani automatické opakování odeslání — je to čistě protokolová vrstva.

Obsah

Podporované runtimes

Runtime Podpora
Node.js 22.18+ (viz engines.node)
Bun aktuální stabilní verze
Deno aktuální stabilní verze
Prohlížeč libovolný moderní, s crypto.subtle — viz Použití v prohlížeči

SDK nikde nepoužívá Node-specifické API (žádný node: import) — jen standardní webové primitivy (fetch, crypto.subtle, Uint8Array, TextEncoder), proto se na všech čtyřech chová stejně. Kompatibilita Node.js/Bun/Deno je ověřená v CI proti stejné testovací sadě (.github/workflows/ci.yml); prohlížeč navíc pokrývá opt-in test v reálném Chromiu (viz Použití v prohlížeči).

Instalace

bun add @finitoapp/eet-client
# nebo
npm install @finitoapp/eet-client
# nebo (Deno)
deno add npm:@finitoapp/eet-client

Rychlý start

import { createEetClient, EetEndpoint } from "@finitoapp/eet-client";
import { parseEetReceiptData } from "@finitoapp/eet-client/builtin";

const client = createEetClient({
  endpoint: EetEndpoint.playground,
  signer, // viz "Signer" níže
  responseSignatureVerifier, // viz "Ověření podpisu odpovědi" níže
});

const parsedReceipt = parseEetReceiptData({
  eic_popl: "CZ8551015704",
  id_jednotky: "181",
  id_pokl: "00/2535/CN58",
  porad_cis: "0/2482/IE25",
  dat_trzby: "2027-01-07T22:01:00+01:00",
  celk_trzba: "87988.00",
});
if (!parsedReceipt.ok) {
  console.error("Neplatná data tržby:", parsedReceipt.error);
  return;
}

const result = await client.submit(parsedReceipt.value, { firstSubmission: true });

if (!result.ok) {
  // result.error je hodnota typu EetError, rozlišitelná podle .type (viz "Typované chyby" níže).
  console.error("submit() selhal:", result.error);
  return;
}

switch (result.value.status) {
  case "accepted":
    console.log("POK:", result.value.pok, "test:", result.value.test);
    break;
  case "verification":
    console.log("Ověřovací mód byl v pořádku zpracován.");
    break;
  case "rejected":
    console.error("EET odmítlo zprávu:", result.value.code, result.value.message);
    break;
}

submit() (a všechny nízkoúrovňové funkce SDK, které mohou selhat) vrací Result<T, E> ({ ok: true, value: T } | { ok: false, error: E }) místo vyhazování výjimek. Balíček k tomu exportuje i samotné pomocníky ok, err, isOk, isErr, getOrThrow, getOrNull, trySync, tryAsync — hodí se i pro vlastní signer/responseSignatureVerifier adaptéry.

@finitoapp/eet-client (hlavní balíček) je validátor-agnostický — nemá vestavěnou žádnou validaci a neví, čím EetReceiptData/EetHeader vznikly. Validaci vybíráte samostatným importem, viz Validace vstupu: @finitoapp/eet-client/builtin (hand-rolled, bez závislostí — použito výše) nebo @finitoapp/eet-client/zod (zod v4).

Vstup parseEetReceiptData odpovídá elementu <Data> z EETXMLSchema.xsd — vlastnosti používají stejná XML jména jako specifikace (eic_popl, id_pokl, ...), aby šly přímo dohledat v dokumentaci GFŘ. Finanční částky (celk_trzba, urceno_cerp_zuct, cerp_zuct) jsou řetězce s právě dvěma desetinnými místy, ne number — zabraňuje to ztrátě přesnosti a formátu. Čas (dat_trzby) je ISO 8601 řetězec s explicitním offsetem; SDK nemění časovou zónu ani neodvozuje lokální čas. Nepovinné vlastnosti se musí zcela vynechat, ne nastavit na undefined — SDK je pak do XML nezapíše.

submit() sám o sobě receipt nevaliduje — bere rovnou obrandovaný EetReceiptData, výstup parseEetReceiptData (nebo libovolného jiného validátoru se stejným výstupním typem, např. zod schématu z @finitoapp/eet-client/zod). Validace receipt je tak vyměnitelná; submit() sám o sobě nevaliduje vůbec nic — options.uuid/options.sentAt, pokud je zadáte, musí být také už obrandované (Uuid/EetDateTime), stejně jako receipt.

uuid_zpravy a dat_odesl (druhý parametr submit) se bezpečně vygenerují z crypto.randomUUID()/aktuálního času, pokud je nepředáte explicitně jako uuid/sentAt — což je potřeba pro opakované odeslání a deterministické testy (viz Opakované odeslání). Vygenerované hodnoty jsou správné ze své podstaty; vlastní uuid/sentAt musíte dodat už jako Uuid/ EetDateTime (např. výstupem parseHeader).

signer z reálného .p12 souboru od GFŘ

Nejběžnější první krok je proměnit .p12/.pfx soubor stažený z DIS+ (viz Získání produkčního pokladního certifikátu) na signer z příkladu výše. Nejrychlejší cesta je vestavěný, čistě JS/Web Crypto parser @finitoapp/eet-client/pkcs12 — funguje stejně v Node.js/Bunu/ Denu i přímo v prohlížeči, bez openssl nebo jiného externího nástroje:

import { readFileSync } from "node:fs";
import { createCryptoKeySigner } from "@finitoapp/eet-client";
import { parsePkcs12, pickPrivateKeyCertificate } from "@finitoapp/eet-client/pkcs12";

const p12 = new Uint8Array(readFileSync("pokladni-cert.p12"));
const parsed = await parsePkcs12(p12, process.env.P12_PASSWORD ?? "");
if (!parsed.ok) throw parsed.error; // Pkcs12InvalidMacError (špatné heslo) / Pkcs12MalformedError

const certificate = pickPrivateKeyCertificate(parsed.value);
if (certificate === undefined || parsed.value.privateKey === undefined) {
  throw new Error("V .p12 souboru chybí certifikát nebo privátní klíč.");
}

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  parsed.value.privateKey.der,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false, // extractable: false — surový klíč pak z CryptoKey už nejde vytáhnout
  ["sign"],
);

const signer = createCryptoKeySigner(certificate.der, privateKey);

Podrobnosti (podporované šifrovací algoritmy .p12 souborů, alternativy přes openssl/node-forge, použití v prohlížeči a bezpečnostní poznámky k heslu) viz Nastavení klíče z reálného pokladního certifikátu (.p12/PFX) níže.

Signer

signer je přenositelný adaptér nad privátním klíčem poplatníka. SDK z něj nikdy nečte, neexportuje ani neukládá privátní klíč — jen si vyžádá DER certifikát a nechá adaptér podepsat bajty:

interface EetSigner {
  getCertificate(): Uint8Array | PromiseLike<Uint8Array>;
  sign(data: Uint8Array): PromiseLike<Uint8Array>; // RSASSA-PKCS1-v1_5 / SHA-256
}

Adaptér tak může být postavený nad CryptoKey (Web Crypto), HSM, KMS nebo jiným bezpečným úložištěm. Pro nejčastější případ — podepisování nad CryptoKey — SDK nabízí hotový helper createCryptoKeySigner, takže adaptér není potřeba psát ručně (funguje v Node.js, Bunu, Denu i prohlížeči):

import { createCryptoKeySigner } from "@finitoapp/eet-client";

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  privateKeyDer,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false,
  ["sign"],
);

const signer = createCryptoKeySigner(certificateDer, privateKey);

Hlavní vstupní bod (@finitoapp/eet-client) sám nenačítá ani nekonvertuje PEM/PFX/PKCS#12 — převod pokladního certifikátu do DER/CryptoKey, který signer potřebuje, řeší samostatný volitelný podcestový modul @finitoapp/eet-client/pkcs12 (viz Nastavení klíče z reálného pokladního certifikátu (.p12/PFX) níže), případně externí nástroj/knihovna dle vlastní volby (viz Bezpečné nakládání s certifikáty).

Získání produkčního pokladního certifikátu

Než bude mít signer co podepisovat, potřebujete od GFŘ vydaný pokladní certifikát — tato SDK ho nevydává ani negeneruje, pouze s ním pracuje. Podle dokumentů „Přístupové a provozní informace — produkční prostředí“ (kap. 3.1) a „CA EET 2 — Postupy získání pokladního certifikátu“ probíhá vydání takto:

  1. Portál. Obslužný portál certifikační autority (CA EET 2) je dostupný přes portál MOJE daně, v aplikaci Daňová informační schránka plus (DIS+). Přístup do DIS+ vyžaduje standardní přihlášení poplatníka (datová schránka, bankovní identita, elektronický občanský průkaz apod. — viz samotný portál MOJE daně).
  2. Počet certifikátů. Poplatník si může vyžádat jeden nebo více pokladních certifikátů; kolik jich bude mít a jak je rozdělí mezi jednotlivé pokladny, je čistě na něm. Limit je max. 10 vydaných certifikátů za den a provozovnu (ochrana proti zahlcení systému).
  3. Vydaný certifikát. RSA 2048, CN = vaše EIČ (Evidenční identifikační číslo přidělené v DIS+), platnost 366 dní — certifikát je tedy nutné každý rok včas obnovit, jinak přestanete moci podepisovat nové datové zprávy. Certifikát vydává EETv2 NCA SubCA RSA ..., nad kterou stojí kořenová EETv2 NCA Root CA RSA ... (dvouúrovňová hierarchie CA EET 2).
  4. Revokace. Pokud certifikát zneplatníte (ztráta klíče, výměna pokladny apod.), CRL na https://caeet.gov.cz/crldp se aktualizuje do 1 minuty; běžná frekvence vydávání CRL je jinak každých 8 hodin.

CA EET klíčový pár i certifikát generuje sama, bezpečně a zcela mimo pokladní systém a prohlížeč — v DIS+ tedy nenahráváte vlastní CSR. Vydaný pokladní certifikát včetně soukromého klíče si stáhnete rovnou jako balíček PKCS#12 (.p12/.pfx), chráněný heslem, které dostanete taktéž v aplikaci Správa pokladních certifikátů EET — stejně jako u sdílených testovacích certifikátů playgroundu (viz caeet/). Soubor je ke stažení nejdéle 30 dní od vydání certifikátu, poté je v systému zrušen a už není možné jej znovu získat. Pokračujte podle sekce Nastavení klíče z reálného pokladního certifikátu (.p12/PFX) níže.

Obnova. Certifikát lze obnovit stejným ručním postupem v DIS+, nebo automatizovaně z pokladního systému přes samostatné REST API CA EET (doporučená lhůta je 2–3 týdny před koncem platnosti obnovovaného certifikátu) — viz Automatizovaná obnova pokladního certifikátu níže pro volitelný klient tohoto API, který je součástí této SDK jako samostatný podcestový import (experimentální, viz varování v té sekci).

Nastavení klíče z reálného pokladního certifikátu (.p12/PFX)

Certifikační autorita vydává pokladní certifikát jako soubor PKCS#12 (.p12/.pfx) chráněný heslem. signer ale čeká holé DER bajty (certifikát a PKCS8 privátní klíč) — .p12 je nutné nejdřív rozbalit. Na výběr jsou tři cesty, popsané níže: vestavěný @finitoapp/eet-client/pkcs12 (doporučeno — čistý JS/Web Crypto, funguje všude včetně prohlížeče), openssl (externí nástroj, jednorázově v shellu) a node-forge (zavedená externí knihovna). Všechny tři vedou ke stejnému výsledku — DER bajtům, které crypto.subtle.importKey("pkcs8", ...)/createCryptoKeySigner čekají.

@finitoapp/eet-client/pkcs12 (doporučeno)

Volitelný podcestový modul, samostatná implementace čtení PKCS#12 (RFC 7292) v čistém JS: žádné node:crypto/node:tls, žádná nativní/WASM závislost — jen globalThis.crypto.subtle, stejně jako zbytek SDK. Hlavní vstupní bod (@finitoapp/eet-client) tento modul nikdy neimportuje.

import { readFileSync } from "node:fs";
import { createCryptoKeySigner } from "@finitoapp/eet-client";
import { parsePkcs12, pickPrivateKeyCertificate } from "@finitoapp/eet-client/pkcs12";

const p12 = new Uint8Array(readFileSync("pokladni-cert.p12"));
const parsed = await parsePkcs12(p12, /* heslo z DIS+/aplikace Správa pokladních certifikátů */ "");
if (!parsed.ok) throw parsed.error; // Pkcs12InvalidMacError (špatné heslo) / Pkcs12MalformedError

// pickPrivateKeyCertificate vybere z (případného) řetězu certifikátů ten, který
// patří k privátnímu klíči (podle localKeyId) — u .p12 s jedním certifikátem
// (typický pokladní certifikát) vrátí prostě ten jediný.
const certificate = pickPrivateKeyCertificate(parsed.value);
if (certificate === undefined || parsed.value.privateKey === undefined) {
  throw new Error("V .p12 souboru chybí certifikát nebo privátní klíč.");
}

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  parsed.value.privateKey.der,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false, // extractable: false
  ["sign"],
);

const signer = createCryptoKeySigner(certificate.der, privateKey);

parsePkcs12 implementuje jen to, co reálné pokladní/playground .p12 soubory od GFŘ skutečně používají — heslem chráněný režim (MacData přes SHA-1), certifikáty šifrované pbeWithSHA1And{40,128}BitRC2-CBC a privátní klíč šifrovaný pbeWithSHA1And{2,3}-KeyTripleDES-CBC (RC2 a DES/3DES jsou proto v src/pkcs12/ hand-rolled — crypto.subtle je vůbec neumí, jde o záměrně starší algoritmy, kterými .p12 soubory šifrují CA nástroje dodnes). Cokoliv mimo tento rozsah (podepsaný/enveloped authSafe, jiné PBE algoritmy) vrátí typovanou Pkcs12MalformedError s OID, ne tichý špatný výsledek — viz zdrojový doc komentář src/pkcs12/parse.ts pro přesný výčet.

Alternativa: openssl

Kdo nechce přidávat další podcestový import, nebo chce rozbalení ověřit nezávisle přes všeobecně známý nástroj, může místo toho použít openssl (mimo prohlížeč — viz V prohlížeči níže). Rozbalte .p12 jednou, přímo v shellu (heslo dejte do souboru, ne jako argument — ten je vidět v seznamu procesů):

# -legacy: starší .p12 soubory šifrují klíč přes RC2-40-CBC, který openssl 3.x
# bez legacy providera nerozbalí.

# certifikát (bez klíče) → PEM
openssl pkcs12 -legacy -in pokladni-cert.p12 -passin file:heslo.txt -nokeys -clcerts -out cert.pem

# privátní klíč (nešifrovaný!) → PEM
openssl pkcs12 -legacy -in pokladni-cert.p12 -passin file:heslo.txt -nocerts -nodes -out key.pem

# crypto.subtle chce DER, ne base64 PEM — převod obou souborů
openssl x509 -in cert.pem -outform der -out cert.der
openssl pkcs8 -topk8 -nocrypt -in key.pem -outform der -out key.der

key.pem/key.der obsahují nešifrovaný privátní klíč — chmod 600, nikdy nekomitujte, přidejte do .gitignore a smažte, jakmile je bezpečně uložíte jinam (HSM, KMS, tajný store apod.).

Výsledné .der soubory pak stačí načíst a předat do crypto.subtle:

import { readFileSync } from "node:fs";
import { createCryptoKeySigner } from "@finitoapp/eet-client";

const certificateDer = new Uint8Array(readFileSync("cert.der"));
const keyDer = new Uint8Array(readFileSync("key.der"));

const privateKey = await crypto.subtle.importKey(
  "pkcs8", keyDer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"],
);

const signer = createCryptoKeySigner(certificateDer, privateKey);

V Denu funguje node:fs import beze změny, skript ale navíc musíte spustit s --allow-read (Deno defaultně blokuje přístup k souborovému systému, dokud ho explicitně nepovolíte).

Kdo .der soubory na disku držet nechce (např. v CI), může místo posledních dvou příkazů zachytit -out/PEM výstup rovnou v paměti přes podproces a zdekódovat base64 tělo mezi -----BEGIN.../-----END... sám — přesně tímto způsobem to (jen pro testy, nikdy natrvalo na disk) dělá test/integration/p12-helper.ts v tomto repozitáři.

Alternativa: node-forge

Kdo preferuje zavedenou, široce používanou JS knihovnu před vestavěným parserem této SDK, může PKCS#12 rozbalit i přes node-forge — umí stejné starší algoritmy (RC2, 3DES) jako reálné .p12 soubory od GFŘ, a funguje jak v Node.js/Bunu/Denu, tak v prohlížeči:

import { readFileSync } from "node:fs";
import forge from "node-forge";
import { createCryptoKeySigner } from "@finitoapp/eet-client";

// forge pracuje s "binárními řetězci" (1 znak = 1 bajt), ne Uint8Array — tenhle
// pár funkcí převádí mezi oběma reprezentacemi.
const toBinaryString = (bytes: Uint8Array) => String.fromCharCode(...bytes);
const toBytes = (binary: string): Uint8Array => {
  const out = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
  return out;
};

const p12Asn1 = forge.asn1.fromDer(toBinaryString(new Uint8Array(readFileSync("pokladni-cert.p12"))));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, "heslo-z-dis-plus");

const certBag = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag]?.[0];
const keyBag = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[
  forge.pki.oids.pkcs8ShroudedKeyBag
]?.[0];
if (certBag?.cert === undefined || keyBag?.key === undefined) {
  throw new Error("V .p12 souboru chybí certifikát nebo privátní klíč.");
}

const certificateDer = toBytes(forge.asn1.toDer(forge.pki.certificateToAsn1(certBag.cert)).getBytes());
const keyDer = toBytes(
  forge.asn1.toDer(forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(keyBag.key))).getBytes(),
);

const privateKey = await crypto.subtle.importKey(
  "pkcs8", keyDer, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"],
);
const signer = createCryptoKeySigner(certificateDer, privateKey);

node-forge je oproti @finitoapp/eet-client/pkcs12 širší, dlouhodobě zavedená knihovna (TLS, X.509, RSA, ...), ale je to netriviální runtime závislost (stovky kB) do nezávislého, samostatně udržovaného balíčku — v rozporu s tím, že tato SDK jinak nemá žádné runtime závislosti (viz úvod). Vestavěný @finitoapp/eet-client/pkcs12 naproti tomu dělá jen to, co je potřeba pro reálné GFŘ .p12 soubory, a je součástí stejné testované a auditovatelné codebase jako zbytek SDK. Volba je na vás — obě cesty vedou ke stejným DER bajtům.

V prohlížeči

openssl (ani node-forge jako Node.js knihovna) v prohlížeči k dispozici nemáte, ale @finitoapp/eet-client/pkcs12 je čistý JS/Web Crypto — .p12 proto jde rozbalit přímo v prohlížeči, stejným kódem jako v Node.js/Bunu/Denu, jen se soubor typicky získá z <input type="file">:

import { createCryptoKeySigner } from "@finitoapp/eet-client";
import { parsePkcs12, pickPrivateKeyCertificate } from "@finitoapp/eet-client/pkcs12";

// file: File z <input type="file"> (nebo drag & drop) — .p12 soubor nikdy
// neopouští prohlížeč, na rozdíl od backendové varianty níže.
const p12 = new Uint8Array(await file.arrayBuffer());
const parsed = await parsePkcs12(p12, password); // password z <input type="password">

if (!parsed.ok) throw parsed.error;
const certificate = pickPrivateKeyCertificate(parsed.value);
if (certificate === undefined || parsed.value.privateKey === undefined) {
  throw new Error("V .p12 souboru chybí certifikát nebo privátní klíč.");
}

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  parsed.value.privateKey.der,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false, // extractable: false — klíč pak z CryptoKey už nejde znovu vytáhnout
  ["sign"],
);

const signer = createCryptoKeySigner(certificate.der, privateKey);

Bezpečnostně konzervativnější nasazení (kde .p12 soubor ani jeho heslo nemá do klientského JS vůbec dorazit) může místo toho zůstat u rozbalení na backendu a poslat do stránky už jen odvozené DER bajty:

// certificateDerBase64/keyDerBase64 přišly z vašeho backendu (autentizovaně,
// přes HTTPS) — nikdy .p12 soubor ani jeho heslo.
const toBytes = (base64: string) => Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));

const privateKey = await crypto.subtle.importKey(
  "pkcs8",
  toBytes(keyDerBase64),
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false,
  ["sign"],
);

const signer = createCryptoKeySigner(toBytes(certificateDerBase64), privateKey);

Pro opakované návštěvy stejné stránky/aplikace zvažte privateKey (samotný CryptoKey, ne surové bajty) rovnou uložit do IndexedDB — prohlížeče CryptoKey umí bezpečně serializovat přes structured clone. Další načtení stránky pak klíč načte přímo z IndexedDB a .p12/PKCS8 bajty se do prohlížeče už nemusí posílat/rozbalovat znovu.

Nezapomeňte i na fetch: window.fetch.bind(window) z Použití v prohlížeči — bez něj submit() v reálném prohlížeči selže bez ohledu na to, jak dobře je signer nastavený.

Ověření podpisu odpovědi

responseSignatureVerifier je povinný asynchronní adaptér. Odpověď se SDK rozparsuje bezpečně (bez DTD/externích entit), ověří strukturu, algoritmy (Exclusive C14N 1.0, SHA-256, RSA-SHA256) a otisk <soap:Body> — adaptéru poté předá už kanonizovaná data k ověření kryptografického podpisu a důvěryhodnosti certifikátu/řetězce:

interface ResponseSignatureVerifier {
  verify(input: {
    raw: string; // syrová SOAP odpověď
    signature: {
      signedBodyCanonical: Uint8Array;
      signedInfoCanonical: Uint8Array;
      signatureValue: Uint8Array;
      digestValue: Uint8Array;
      certificates: readonly Uint8Array[]; // DER, leaf certifikát první
      canonicalizationAlgorithm: string;
      digestAlgorithm: string;
      signatureAlgorithm: string;
    };
  }): PromiseLike<boolean>;
}

Pokud ověření selže, adaptér vrátí false/vyhodí chybu, nebo adaptér není dodán vůbec, submit() nikdy nevrátí accepted výsledek — SDK vrátí Err(EetSignatureError). Pro nejčastější případ — jeden pevně důvěryhodný certifikát (princip pinningu) — SDK nabízí hotový helper createCryptoKeyResponseSignatureVerifier:

import { createCryptoKeyResponseSignatureVerifier } from "@finitoapp/eet-client";

const verifier = createCryptoKeyResponseSignatureVerifier(
  trustedPublicKeySpkiDer,
  trustedCertificateDer,
);

publicKey (první argument) může být i už naimportovaný CryptoKey — surové SPKI DER bajty si helper naimportuje sám, jednou, líně při prvním verify() volání.

Pinning ≠ chain-of-trust validace: helper porovná leaf certifikát z odpovědi bajt po bajtu s trustedCertificateDer a ověří RSA-SHA256 podpis proti publicKey — nekontroluje vydavatele, platnost ani revokaci a nemá fallback na kořenovou CA. Pokud chcete ověřovat celý řetězec, napište si vlastní adaptér implementující rozhraní výše.

Toto omezení není nedodělek, ale záměrné rozhodnutí. Podpisový certifikát odpovědí GFŘ totiž nepochází z hierarchie EET CA (té, kterou najdete v caeet/ a která vydává pokladní certifikáty) — podle přístupových a provozních informací playgroundu (kap. 3.3) ho vydává komerční autorita I.CA (První certifikační autorita, a.s.), CN = I.CA Public CA/RSA 06/2022. Root ani intermediate certifikáty I.CA nejsou součástí žádného EET dokumentu ani distribuce — GFŘ na ně jen odkazuje (https://www.ica.cz/korenove-certifikaty), bez jakékoli záruky, jak často se budou obnovovat nebo jak stabilní jejich vydávání je. Plná chain-of-trust validace v této SDK by proto znamenala natrvalo bundlovat a udržovat trust anchor třetí strany, kterou EET protokol nijak neřídí (a intermediate certifikáty se běžně obměňují v řádu let — viz 06/2022 v názvu). K tomu by přibyla ještě revokace (CRL/OCSP na straně I.CA), která vyžaduje síťové volání a rozhodnutí, jak se zachovat při jeho výpadku — a to uvnitř funkce, která je dnes čistě kryptografická a synchronní. Právě tahle oblast (validace certifikátových řetězců) je notoricky náchylná k subtilním chybám, takže napůl hotová implementace by byla horší než jasně deklarovaný pinning. Komunikační kanál je navíc už chráněný povinným HTTPS (TLS 1.2+, EV certifikát) — podpis odpovědi je z hlediska identity endpointu jen dodatečná vrstva, ne jediná obrana.

Adresář caeet/ obsahuje kořenový a podřízený CA certifikát neprodukčního prostředí (playground) — použijte je jen jako testovací trust anchor pro responseSignatureVerifier ve vývoji/testech, nikdy v produkci.

Výsledky submit()

submit() vrací Result<EetSubmitOutcome, EetError> — zkontrolujte result.ok dřív, než sáhnete na result.value/result.error. Chyby sítě/protokolu se nikdy nevyhazují jako výjimka, vždy dorazí jako result.error (hodnota typu EetError, viz níže). result.value.status je pak diskriminovaný union podle status, nikoli nejednoznačný objekt.

status Význam
"accepted" Tržba byla přijata a zaevidována (nebo playground ekvivalent). Obsahuje pok, uuid, receivedAt, test, warnings. Podpis odpovědi byl úspěšně ověřen.
"verification" Úspěšný běh v ověřovacím módu (overeni: true) — odpověď <Chyba kod="0">. Nic se nezaevidovalo. Obsahuje test, warnings, případně uuid.
"rejected" EET odmítlo zprávu (nenulový chybový kód) nebo ověřovací mód selhal. Obsahuje code, message, test, warnings, případně uuid/rejectedAt.

Všechny tři varianty nesou httpStatus a globalTransactionId (hodnota hlavičky X-Global-Transaction-Id), je-li k dispozici — hodí se k dohledání problému u GFŘ, zejména na playgroundu.

Chybové (Chyba) odpovědi z EET jsou dle specifikace vždy bez elektronického podpisu — to platí i pro "verification"/"rejected", proto tyto dvě varianty responseSignatureVerifier nevyžadují ani nekontaktují.

Typované chyby (result.error)

EetError je diskriminovaný union prostých objektů ({ type, ...pole }), vytvářených přes defineError (viz src/error.ts) — žádná chybová třída se nikde nededí ani nevyhazuje jako výjimka. Rozlišujte podle result.error.type (nebo pomocí exportovaného typového strážce isEetError(error, "EetXmlError")):

type Kdy nastane
"EetValidationError" Vstupní receipt/submit() volby nevyhověly lokální validaci (regulární výrazy, meze XSD, ASCII).
"EetMessageTooLargeError" Podepsaná SOAP obálka přesáhla limit 12 kB.
"EetNetworkError" fetch selhal (DNS, spojení, TLS, ...).
"EetTimeoutError" Vypršel timeoutMs, nebo byl požadavek zrušen.
"EetHttpError" Neočekávaný HTTP status a tělo nebylo rozpoznatelné jako SOAP Fault/Odpoved.
"EetSoapFaultError" Server vrátil SOAP 1.1 <Fault>.
"EetXmlError" Tělo odpovědi nebylo platné XML (nebo obsahovalo DOCTYPE/neznámou entitu).
"EetResponseSchemaError" XML nevyhovělo očekávané struktuře/jmennému prostoru EET (např. >10 Varovani).
"EetSignatureError" Podpis odpovědi chybí, používá jiný algoritmus, neodpovídá otisku, nebo responseSignatureVerifier vrátil/vyhodnotil neúspěch.
"EetSignerError" signer.sign()/signer.getCertificate() vyhodil výjimku nebo byl jeho slib zamítnut (např. HSM/KMS selhání).

Většina variant nese i message a nepovinné httpStatus/globalTransactionId (sdílený tvar EetErrorContext), je-li k dispozici. Každý typ chyby má i svou tovární funkci (createEetValidationError, createEetXmlError, ...), kterou lze použít i mimo SDK, např. ve vlastním signer/responseSignatureVerifier.

Samostatné pomocné funkce

Vedle createEetClient SDK exportuje i nízkoúrovňové stavební bloky, aby šlo použít vlastní transport nebo signer bez duplikace protokolové logiky:

  • buildTrzbaElement, buildUnsignedEnvelope, serializeUnsignedRequest — sestavení nepodepsané EET XML/SOAP zprávy; berou už obrandovaná/validovaná data (výstup validátoru, viz Validace vstupu), ne syrový vstup.
  • parseAndVerifyResponse — parsování a ověření syrové SOAP odpovědi, vrací Result<EetSubmitOutcome, EetError>.
  • createCryptoKeySigner, createCryptoKeyResponseSignatureVerifier — hotové adaptéry nad Web Crypto (CryptoKey + DER certifikát), viz Signer a Ověření podpisu odpovědi.

Validace vstupu

@finitoapp/eet-client (hlavní balíček) neobsahuje žádnou validaci — submit() bere rovnou obrandovaný EetReceiptData/EetHeader a je mu jedno, čím vznikly. Validaci si vyberete samostatným podcestovým importem; obě cesty prosazují stejná pravidla (stejné regexy z EETXMLSchema.xsd, stejná kontrola kalendářního data u dateTime) a produkují tytéž obrandované typy, takže jsou vzájemně zaměnitelné a se zbytkem SDK (submit(), buildTrzbaElement, ...) kompatibilní obě:

  • @finitoapp/eet-client/builtin — hand-rolled, bez runtime závislostí. parseEetReceiptData, parseHeader vrací Result<T, EetValidationError> s brandovanými typy jako TaxPayerId, Amount, Uuid, ...
  • @finitoapp/eet-client/zod — schémata pro zod v4, pro koho preferuje zod. Popsáno níže.

Ani jeden z modulů se do vašeho bundlu nedostane, pokud ho neimportujete — main entry point (@finitoapp/eet-client) na žádný z nich neváže.

@finitoapp/eet-client/builtin

import { parseEetReceiptData, parseHeader } from "@finitoapp/eet-client/builtin";

const parsedReceipt = parseEetReceiptData({
  eic_popl: "CZ8551015704",
  id_jednotky: "181",
  id_pokl: "00/2535/CN58",
  porad_cis: "0/2482/IE25",
  dat_trzby: "2027-01-07T22:01:00+01:00",
  celk_trzba: "87988.00",
});
if (!parsedReceipt.ok) {
  console.error("Neplatná data tržby:", parsedReceipt.error);
  return;
}

@finitoapp/eet-client/zod

zod je peerDependencies (volitelná — peerDependenciesMeta.zod.optional), ne běžná závislost: pokud @finitoapp/eet-client/zod neimportujete, zod se do vašeho bundlu vůbec nedostane. Kdo tuto cestu chce použít, nainstaluje si zod sám:

bun add zod
# nebo
npm install zod
import { createEetClient, EetEndpoint } from "@finitoapp/eet-client";
import { EetReceiptDataZodSchema } from "@finitoapp/eet-client/zod";

const parsedReceipt = EetReceiptDataZodSchema.safeDecode({
  eic_popl: "CZ8551015704",
  id_jednotky: "181",
  id_pokl: "00/2535/CN58",
  porad_cis: "0/2482/IE25",
  dat_trzby: "2027-01-07T22:01:00+01:00",
  celk_trzba: "87988.00",
});
if (!parsedReceipt.success) {
  console.error("Neplatná data tržby:", parsedReceipt.error);
  return;
}

Balíček exportuje jen syrová schémata EetReceiptDataZodSchema/ EetHeaderZodSchema — žádné Result-vracející wrappery. Zpracování výsledku (.decode()/.safeDecode(), převod ZodError na váš vlastní typ chyby) je na vás; schémata se dají i rovnou složit do vlastního, většího zod schématu (z.object({ ..., data: EetReceiptDataZodSchema })).

Doporučujeme .safeDecode()/.decode() místo .safeParse()/.parse(): obě dvojice za běhu validují úplně stejně (v zodu v4 je safeDecode interně alias na safeParse), ale liší se v typování vstupního argumentu — safeParse(data: unknown, ...) přijme na vstupu cokoliv, zatímco safeDecode(data: core.input<this>, ...) vyžaduje, aby vstup odpovídal shape a základním typům schématu už podle TypeScriptu. Špatný shape/typ tak odhalí typecheck, ne až běhová validace.

Opakované odeslání

SDK neprovádí žádné automatické opakování ani perzistentní frontu. Není to opomenutí: server nededukuje podle uuid_zpravy ani podle obchodní n-tice tržby (viz kapitola 4 specifikace) — každé odeslání, i duplicitní, dostane vlastní POK. Po EetNetworkError/EetTimeoutError navíc není jisté, zda zpráva k serveru vůbec nedorazila, nebo dorazila a jen se ztratila odpověď. Slepé automatické zopakování by tak mohlo tržbu nahlásit dvakrát; zda a kdy odeslání opakovat je proto rozhodnutí aplikace, která zná kontext (vydanou účtenku, vlastní frontu, lhůty), ne SDK. Přesné chování serveru u duplicitního hlášení se aktuálně ověřuje e-mailovou komunikací se správcem daně; až budou závěry k dispozici, tato sekce se podle nich upřesní.

Při opakovaném odeslání téže tržby (např. po EetNetworkError/EetTimeoutError nebo -1/dočasné technické chybě):

  • vygenerujte nové uuid_zpravy (nepředávejte staré uuid),
  • nastavte firstSubmission: false,
  • ostatní údaje o tržbě (eic_popl, id_jednotky, id_pokl, porad_cis, dat_trzby, celk_trzba, ...) zůstávají stejné — jsou to ony, ne UUID, co určuje unikátnost evidované tržby (viz kapitola 4 specifikace).

Bezpečné nakládání s certifikáty

  • SDK nikdy nevyžaduje, nenačítá, neexportuje ani neukládá privátní klíč poplatníka — pouze DER certifikát (signer.getCertificate()) a podpis bajtů (signer.sign()). Formát/úložiště klíče (PEM, PKCS#12, HSM, KMS, CryptoKey v IndexedDB, ...) je zcela na integrátorovi.
  • Adresář caeet/ obsahuje sdílené přístupové údaje playgroundu (*.p12 a soubor s heslem) zveřejněné GFŘ pro vývojáře. Reálné riziko úniku je nízké — playground nemá právní ani fiskální účinek — přesto je nenačítáme do produkčního kódu, nekopírujeme do distribuce ani testovacích výstupů a nelogujeme: jde o obecnou hygienu nakládání s privátním klíčem a certifikátem, kterou má SDK modelovat správně i pro případ, že integrátor stejný vzor použije se svým reálným produkčním certifikátem.
  • SDK nikdy nezaznamenává (neloguje) obsah privátního klíče, SignatureValue ani syrovou odpověď mimo to, co si explicitně vyžádá vaše volání.

Model důvěry createCryptoKeySigner/createCryptoKeyResponseSignatureVerifier

EetSigner a ResponseSignatureVerifier jsou čistě abstraktní rozhraní (viz Signer, Ověření podpisu odpovědi) — createCryptoKeySigner/createCryptoKeyResponseSignatureVerifier jsou jen jedna z možných implementací, ne povinná součást. Kdo SDK (nebo jeho supply chain) nechce svěřit přístup ke CryptoKey, může si obě rozhraní napsat sám, ve vlastním důvěryhodném kódu — crypto.subtle.sign/crypto.subtle.verify zavolá tam a CryptoKey do SDK vůbec nepředá.

Co helpery reálně (ne)riskují:

  • Únik klíče. Pokud privateKey importujete s extractable: false (viz výše), surový klíčový materiál z CryptoKey nejde vytáhnout, ani kdyby byl balíček kompromitovaný. Platí bez ohledu na to, jestli EetSigner napíšete sami, nebo použijete createCryptoKeySigner.
  • Zneužití jako podpisové orákulum. createCryptoKeySigner drží referenci na privateKey po celou dobu své existence a zavolá crypto.subtle.sign nad čímkoliv, co mu submit() pošle k podpisu. extractable: false tomu nezabrání — brání jen exportu klíče, ne jeho použití. Toto riziko helper neřeší; pokud ho chcete eliminovat, ne jen omezit, implementujte EetSigner sami a validujte/logujte data před podpisem.
  • U createCryptoKeyResponseSignatureVerifier je dopad jiný: nejde o tajemství (jen veřejný klíč/certifikát), ale o integritu rozhodnutí — kompromitovaná implementace by mohla vždy vrátit true. Stejná záchranná brzda platí: napište si vlastní ResponseSignatureVerifier, pokud toto riziko nechcete nést.

Hotové helpery jsou vhodné, pokud SDK jako celku důvěřujete (auditovali jste ho, máte lockfile a integrity checks v CI). Bezpečnostně citlivější nasazení (velké objemy, klíč sdílený napříč pokladnami) by mělo zvážit vlastní implementaci obou rozhraní, případně v izolovaném modulu/procesu.

Opt-in integrační testy s caeet/*.p12

test/integration/ obsahuje čtyři kategorie testů používajících skutečné pokladní certifikáty z caeet/*.p12 a/nebo živou síť. Jsou vypnuté výchozím bun test i v CI (soubory .p12 se v nich nikdy nezaznamenávají do výstupu ani na disk — extrakce běží v paměti přes openssl pkcs12) a zapínají se explicitně proměnnou prostředí — pro každou kategorii existuje i vlastní npm/bun script:

# Podepsání skutečným pokladním klíčem/certifikátem a kontrola řetězce důvěry
# vůči caeet/ca_eet-*.crt — bez síťového volání. Součástí je i kontrola, že
# caeet/*.p12 obsahuje bajtově stejný certifikát jako reálné zprávy zachycené
# v docs/reference/eet-2.0/*.eet.v4.req.xml.
bun run test:integration:p12       # EET_TEST_P12=1 bun test test/integration

# Ověření, že reálný endpoint EET posílá CORS hlavičky povolující volání z
# libovolného origin (viz "Použití v prohlížeči" výše) — bez pokladního
# certifikátu, jen síť.
bun run test:integration:cors      # EET_TEST_CORS=1 bun test test/integration

# Živé odeslání na https://pg.trzbyeet.gov.cz z Bunu a kryptografické ověření
# reálné podepsané odpovědi (vyžaduje síť a openssl s legacy providery v PATH).
bun run test:integration:live      # EET_TEST_LIVE_PLAYGROUND=1 bun test test/integration

# Stejné živé odeslání, ale spuštěné celé (podpis, fetch, ověření) uvnitř
# reálného headless Chromia přes Playwright (vyžaduje navíc
# `bunx playwright install chromium`).
bun run test:integration:live-browser  # EET_TEST_LIVE_PLAYGROUND_BROWSER=1 bun test test/integration

Automatizovaná obnova pokladního certifikátu (@finitoapp/eet-client/caeet-renewal)

Experimentální — zatím neověřeno proti žádnému skutečnému prostředí. docs/reference/eet-2.0/caeetapi_jwt.yml (OpenAPI popis tohoto API) jmenuje tři prostředí — test.caeet.gov.cz, zkus.caeet.gov.cz a produkční caeet.gov.cz — ale k 26. 7. 2026 žádné z nich nemá veřejný DNS záznam (ověřeno přímo dotazem na 8.8.8.8; zóna caeet.gov.cz je delegovaná, ale bez A/AAAA/CNAME záznamu na kterékoli z těchto jmen). Na rozdíl od EET SOAP odesílání tržby, kde je playground (pg.trzbyeet.gov.cz) reálně dostupný a tato SDK proti němu má opt-in integrační testy (viz Opt-in integrační testy s caeet/*.p12), tento modul nebyl ověřen jediným skutečným HTTP voláním — implementace vychází čistě z referenčního dokumentu a docs/reference/eet-2.0/caeetapi_jwt.yml. Očekávejte, že se po prvním reálném ověření proti nasazenému prostředí objeví breaking changes (chybný odhad tvaru chyby, HTTP chování, apod.) — nezávisle na verzování zbytku SDK (viz Verzování).

Volitelný podcestový modul pro automatizovanou obnovu pokladního certifikátu přes REST API CA EET popsané v „CA EET 2 — Postupy získání pokladního certifikátu“. Jde o úplně jiný protokol než EET SOAP odeslání tržby — JWT/REST vůči CA EET, ne SOAP/XMLDSig vůči trzbyeet.gov.cz — hlavní vstupní bod @finitoapp/eet-client tento modul nikdy neimportuje.

import { createCaeetRenewalClient } from "@finitoapp/eet-client/caeet-renewal";

const renewal = createCaeetRenewalClient({
  baseUrl: "https://caeet.example/api", // viz "Neznámé/nepublikované" níže
  signer, // podepisuje JWT certifikátem, který se obnovuje — stejný EetSigner jako pro submit()
});

const request = await renewal.requestRenewal();
if (!request.ok) throw request.error;

let status = await renewal.getStatus(request.value.reqId);
// INPROCESS se pozná podle přítomnosti pollAfterSeconds/retryAfterSeconds (jediná typovaná pole,
// která tato SDK ze status odpovědi čte — viz "Neznámé/nepublikované" níže, proč nehádá pole se
// samotným stavem).
while (
  status.ok &&
  (status.value.pollAfterSeconds !== undefined || status.value.retryAfterSeconds !== undefined)
) {
  const delaySeconds = status.value.pollAfterSeconds ?? status.value.retryAfterSeconds ?? 30;
  await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
  status = await renewal.getStatus(request.value.reqId);
}
if (!status.ok) throw status.error;
// status.value.raw je teď RequestStatusDTO mimo INPROCESS — ISSUED, DELIVERING, FINISHED, nebo
// REJECTED. Tato SDK obsah nehádá (viz níže), takže si stav ověřte sami dle skutečné odpovědi
// nebo caeetapi_jwt.yml; u REJECTED nepokračujte na claimPkcs12() níže (podle referenčního
// dokumentu je platný jen pro ISSUED/DELIVERING).

const claim = await renewal.claimPkcs12(request.value.reqId);
if (!claim.ok) throw claim.error;
// claim.value.raw je Pkcs12DTO (PKCS#12 v base64 + heslo) — po bezpečném
// uložení potvrďte stažení, viz "Neznámé/nepublikované" níže.
await renewal.ackDownload(request.value.reqId);

buildCaeetAuthorizationJwt(signer) (export tohoto modulu) sestaví autentizační JWT přesně dle specifikace — hlavička alg: RS256, typ: JWT, x5t#S256 (otisk certifikátu), minimální payload exp/iat — a podepíše ji přes signer.sign(). EetSigner použitý pro submit() tak jde použít i tady beze změny: RS256 je stejné RSASSA-PKCS1-v1_5/SHA-256 podepisování, které SDK už vyžaduje pro <Trzba>.

Platnost tokenu (exp - iat) lze nastavit volitelnou položkou ttlSeconds v options klienta — výchozí je 60 s, server odmítá cokoliv nad 300 s. Zvyšte ji, pokud signer.sign() (např. vzdálený HSM/KMS) spolu se síťovým zpožděním k serveru CA EET může trvat blízko výchozí hranici — jinak riskujete, že token vyprší dřív, než server stihne požadavek ověřit.

Žádné cachování JWT mezi voláními — záměrně. Každé volání klienta (requestRenewal(), getStatus(), claimPkcs12(), ackDownload(), listUnfinished()) si podepíše vlastní čerstvý token, i v rámci pollovací smyčky. Zvažovali jsme cache platného tokenu mezi voláními, ale zůstali jsme u bezstavového podepisování při každém volání: výchozí platnost tokenu (ttlSeconds, viz výše) je jen 60 s, takže cache by se v praxi často stejně neuplatnila dřív, než token vyprší, a přínos by tak byl nejistý vůči přidané komplexitě (sledování expirace, invalidace při změně ttlSeconds). Pokud váš signer je nákladný (např. vzdálený HSM/KMS) a víte, že server vrací krátké pollovací intervaly, zvažte cachování na vlastní straně nad tímto klientem.

Bez vestavěné smyčky/retry. Stejně jako submit() (viz Opakované odeslání), ani tento modul sám nepolluje — voláte getStatus() v intervalu podle pollAfterSeconds/hlavičky Retry-After, dokud žádost neopustí stav INPROCESS, přesně podle „Příklad sekvence volání“ ve zdrojovém dokumentu.

Neznámé/nepublikované — čtěte, než použijete:

  • baseUrl není nikde publikované. Zdrojový dokument říká jen, že „URL pro přístup k API jsou součástí definice API“ (souboru docs/reference/eet-2.0/caeetapi_jwt.yml), který ale s tímto SDK není distribuovaný ani jinak veřejně dostupný — stejně jako u produkčního EET SOAP endpointu musíte baseUrl získat a dodat sami.
  • Přesný tvar odpovědi mimo reqId/pollAfterSeconds není publikovaný. Zdrojový dokument doslovně jmenuje jen .reqId (POST /request/renew) a .pollAfterSeconds (GET .../status) — stavový výčet (INPROCESS/ISSUED/DELIVERING/FINISHED/REJECTED) i obsah Pkcs12DTO (PKCS#12 v base64, heslo, metadata) popisuje jen prózou, bez jmen polí. Tato SDK proto tato pole nehádá: getStatus() vrací typovaně jen pollAfterSeconds/retryAfterSeconds, zatímco claimPkcs12(), ackDownload() a listUnfinished() vrací syrové raw: unknown tělo — rozeberte si ho sami, jakmile budete mít reálnou odpověď nebo skutečný docs/reference/eet-2.0/caeetapi_jwt.yml.

Data vrácená z claimPkcs12() jsou vysoce citlivá (heslem chráněný privátní klíč) — nikdy je nezalogujte ani necachujte, stejná hygiena jako u Bezpečné nakládání s certifikáty výše.

Použití v prohlížeči

SDK funguje i přímo v prohlížeči — podpis přes Web Crypto (crypto.subtle, viz Signer) i parsování/ověření odpovědi jsou čistě standardní browser API, bez jakékoli Node-only závislosti. Dvě věci, na které si dát pozor:

  1. Vlastní fetch je nutný. Prohlížečový fetch je "branded" metoda vyžadující this === window — zavoláte-li ji odděleně od window (přesně to dělá výchozí options.fetch ?? fetch v createEetClient), skončí to chybou TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation. V prohlížeči proto vždy předejte svázanou verzi:

    const client = createEetClient({
      endpoint: EetEndpoint.playground,
      signer,
      responseSignatureVerifier,
      fetch: window.fetch.bind(window),
    });
  2. CORS. pg.trzbyeet.gov.cz posílá CORS hlavičky (Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: POST, OPTIONS) povolující volání z libovolného origin, takže submit() lze volat přímo z prohlížečové stránky bez vlastního backendu/proxy (ověřeno testem test/integration/browser-cors.test.ts).

test/integration/browser-live-playground.test.ts dokazuje, že celý běh (podpis, fetch, parsování, ověření podpisu) skutečně proběhne v reálném Chromiu, s plným vynucováním CORS (viz Opt-in integrační testy výše).

Architektura

Implementace je rozdělena do vrstev, každá bez znalosti detailů ostatních:

  • model a validace (src/core/patterns.ts, src/builtin/validate.ts, src/zod/) — regulární výrazy a meze převzaté z EETXMLSchema.xsd; core/patterns.ts je sdílí oba validátory.
  • XML/kanonizace (src/core/xml/) — minimální bezpečný XML parser (bez DTD/externích entit) a implementace Exclusive XML Canonicalization 1.0 pro úzkou množinu struktur, které EET 2.0 skutečně používá (bez InclusiveNamespaces PrefixList a bez dědění xml:lang/xml:space/xml:base odjinud než z podepisovaného podstromu — viz doc komentář v src/core/xml/c14n.ts).
  • sestavení a podpis požadavku (src/core/build-request.ts, src/core/sign.ts) — WS-Security hlavička, BinarySecurityToken, ds:Signature.
  • HTTP transport (src/core/transport.ts) — hlavičky, limit velikosti, timeout/abort.
  • parsování a ověření odpovědi (src/core/parse-response.ts) — SOAP Fault/Odpoved/Potvrzeni/Chyba/Varovani, ověření algoritmů a otisku, delegace kryptografického ověření na responseSignatureVerifier.

Veškerý XML vstup z odpovědí se parsuje bezpečně (DOCTYPE a jakékoli entity mimo předdefinovaných/číselných odkazů jsou odmítnuty) a před vrácením accepted výsledku se ověří UUID, jmenný prostor, struktura i podpis.

Rozsah první verze

Není součástí: automatické retry/fronta, doménové workflow pokladny/účtenky, produkční endpoint (dokud jej GFŘ oficiálně nezveřejní — vždy jej dodá integrátor konfigurací). Načítání PKCS#12 (.p12/PFX) má vlastní, samostatný podcestový modul (@finitoapp/eet-client/pkcs12, viz Nastavení klíče z reálného pokladního certifikátu (.p12/PFX)) — hlavní vstupní bod ho stále neimportuje. Automatizovaná obnova pokladního certifikátu má vlastní, samostatný podcestový klient (viz Automatizovaná obnova pokladního certifikátu), ten je ale experimentální — zatím neověřený proti žádnému skutečnému prostředí (viz varování v té sekci).

Verzování

EET playground API zatím není ze strany GFŘ (provozovatele EET) prohlášené za stabilní, a proto ani tento projekt nechceme prohlašovat za stabilní — na druhou stranu playground už je k dispozici, takže knihovna se dá začít používat už teď. Volíme proto standardní semver 0.x.y po celou přípravnou fázi: breaking changes zvyšují minor (0.1.0, 0.2.0, ...), ne patch. 0.0.x vědomě nepoužíváme jako "přípravnou" řadu samo o sobě — npm/bun k ^0.0.x rozsahům přistupují jako k přesnému pinu bez auto-update, což by šlo proti záměru nechat knihovnu už teď reálně zkoušet.

1.0.0 je rezervovaná pro první release po oficiálním vyhlášení stability EET API ze strany GFŘ — to bude první opravdu vážně míněný stable release, ne interně zvolené datum.

Vývoj

Projekt používá Bun jako package manager a runtime pro vývojové nástroje.

bun install
bun run check       # lint + typecheck + testy
bun run build       # ESM/CJS + typy do dist/
bun run check:package

Přispívání

Než přidáte novou funkčnost, otevřete prosím issue — viz CONTRIBUTING.md. Účast v projektu se řídí CODE_OF_CONDUCT.md. Bezpečnostní zranitelnosti hlaste dle SECURITY.md, ne veřejným issue.

Licence

MIT

About

Low-level TypeScript SDK for EET 2.0 (Elektronická evidence tržeb)

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages