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.
- Podporované runtimes
- Instalace
- Rychlý start
- Signer
- Ověření podpisu odpovědi
- Výsledky
submit() - Samostatné pomocné funkce
- Validace vstupu
- Opakované odeslání
- Bezpečné nakládání s certifikáty
- Automatizovaná obnova pokladního certifikátu (
@finitoapp/eet-client/caeet-renewal) — experimentální - Použití v prohlížeči
- Architektura
- Rozsah první verze
- Verzování
- Vývoj
- Přispívání
- Licence
| 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).
bun add @finitoapp/eet-client
# nebo
npm install @finitoapp/eet-client
# nebo (Deno)
deno add npm:@finitoapp/eet-clientimport { 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).
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 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).
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:
- 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ě).
- 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).
- 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). - Revokace. Pokud certifikát zneplatníte (ztráta klíče, výměna pokladny
apod.), CRL na
https://caeet.gov.cz/crldpse 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).
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í.
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.
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.derkey.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.
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.
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ý.
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.
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í.
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.
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.
@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,parseHeadervracíResult<T, EetValidationError>s brandovanými typy jakoTaxPayerId,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.
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;
}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 zodimport { 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.
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).
- 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,CryptoKeyvIndexedDB, ...) je zcela na integrátorovi. - Adresář
caeet/obsahuje sdílené přístupové údaje playgroundu (*.p12a 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,
SignatureValueani syrovou odpověď mimo to, co si explicitně vyžádá vaše volání.
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
privateKeyimportujete sextractable: false(viz výše), surový klíčový materiál zCryptoKeynejde vytáhnout, ani kdyby byl balíček kompromitovaný. Platí bez ohledu na to, jestliEetSignernapíšete sami, nebo použijetecreateCryptoKeySigner. - Zneužití jako podpisové orákulum.
createCryptoKeySignerdrží referenci naprivateKeypo celou dobu své existence a zavolácrypto.subtle.signnad čímkoliv, co musubmit()pošle k podpisu.extractable: falsetomu nezabrání — brání jen exportu klíče, ne jeho použití. Toto riziko helper neřeší; pokud ho chcete eliminovat, ne jen omezit, implementujteEetSignersami a validujte/logujtedatapřed podpisem. - U
createCryptoKeyResponseSignatureVerifierje dopad jiný: nejde o tajemství (jen veřejný klíč/certifikát), ale o integritu rozhodnutí — kompromitovaná implementace by mohla vždy vrátittrue. 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.
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/integrationExperimentá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:
baseUrlnení nikde publikované. Zdrojový dokument říká jen, že „URL pro přístup k API jsou součástí definice API“ (souborudocs/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ítebaseUrlzískat a dodat sami.- Přesný tvar odpovědi mimo
reqId/pollAfterSecondsnení publikovaný. Zdrojový dokument doslovně jmenuje jen.reqId(POST /request/renew) a.pollAfterSeconds(GET .../status) — stavový výčet (INPROCESS/ISSUED/DELIVERING/FINISHED/REJECTED) i obsahPkcs12DTO(PKCS#12 v base64, heslo, metadata) popisuje jen prózou, bez jmen polí. Tato SDK proto tato pole nehádá:getStatus()vrací typovaně jenpollAfterSeconds/retryAfterSeconds, zatímcoclaimPkcs12(),ackDownload()alistUnfinished()vrací syrovéraw: unknowntě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.
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:
-
Vlastní
fetchje nutný. Prohlížečovýfetchje "branded" metoda vyžadujícíthis === window— zavoláte-li ji odděleně odwindow(přesně to dělá výchozíoptions.fetch ?? fetchvcreateEetClient), skončí to chybouTypeError: 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), });
-
CORS.
pg.trzbyeet.gov.czposílá CORS hlavičky (Access-Control-Allow-Origin: *,Access-Control-Allow-Methods: POST, OPTIONS) povolující volání z libovolného origin, takžesubmit()lze volat přímo z prohlížečové stránky bez vlastního backendu/proxy (ověřeno testemtest/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).
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é zEETXMLSchema.xsd;core/patterns.tsje 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á (bezInclusiveNamespacesPrefixList a bez děděníxml:lang/xml:space/xml:baseodjinud než z podepisovaného podstromu — viz doc komentář vsrc/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í naresponseSignatureVerifier.
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.
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).
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.
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:packageNež 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.