@kreiseck/kasseneck-api 0.6.51 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +138 -1
  3. package/dist/cjs/client/aufrufe.d.ts +1 -1
  4. package/dist/cjs/client/aufrufe.js +19 -0
  5. package/dist/cjs/client/errors.d.ts +32 -4
  6. package/dist/cjs/client/errors.js +98 -5
  7. package/dist/cjs/client/transport.js +7 -7
  8. package/dist/cjs/partner/ablauf.d.ts +47 -0
  9. package/dist/cjs/partner/ablauf.js +99 -0
  10. package/dist/cjs/partner/api.d.ts +68 -0
  11. package/dist/cjs/partner/api.js +44 -0
  12. package/dist/cjs/partner/auth.d.ts +33 -0
  13. package/dist/cjs/partner/auth.js +52 -0
  14. package/dist/cjs/partner/betrieb.d.ts +48 -0
  15. package/dist/cjs/partner/betrieb.js +136 -0
  16. package/dist/cjs/partner/endpunkte.d.ts +142 -0
  17. package/dist/cjs/partner/endpunkte.js +488 -0
  18. package/dist/cjs/partner/fehler.d.ts +72 -0
  19. package/dist/cjs/partner/fehler.js +199 -0
  20. package/dist/cjs/partner/index.d.ts +28 -0
  21. package/dist/cjs/partner/index.js +84 -0
  22. package/dist/cjs/partner/secret.d.ts +66 -0
  23. package/dist/cjs/partner/secret.js +95 -0
  24. package/dist/cjs/partner/typen.d.ts +399 -0
  25. package/dist/cjs/partner/typen.js +33 -0
  26. package/dist/cjs/partner/webhook-signatur.d.ts +95 -0
  27. package/dist/cjs/partner/webhook-signatur.js +158 -0
  28. package/dist/cjs/partner/webhooks.d.ts +211 -0
  29. package/dist/cjs/partner/webhooks.js +269 -0
  30. package/dist/esm/client/aufrufe.d.ts +1 -1
  31. package/dist/esm/client/aufrufe.js +19 -0
  32. package/dist/esm/client/errors.d.ts +32 -4
  33. package/dist/esm/client/errors.js +97 -5
  34. package/dist/esm/client/transport.js +8 -8
  35. package/dist/esm/partner/ablauf.d.ts +47 -0
  36. package/dist/esm/partner/ablauf.js +95 -0
  37. package/dist/esm/partner/api.d.ts +68 -0
  38. package/dist/esm/partner/api.js +41 -0
  39. package/dist/esm/partner/auth.d.ts +33 -0
  40. package/dist/esm/partner/auth.js +48 -0
  41. package/dist/esm/partner/betrieb.d.ts +48 -0
  42. package/dist/esm/partner/betrieb.js +132 -0
  43. package/dist/esm/partner/endpunkte.d.ts +142 -0
  44. package/dist/esm/partner/endpunkte.js +474 -0
  45. package/dist/esm/partner/fehler.d.ts +72 -0
  46. package/dist/esm/partner/fehler.js +189 -0
  47. package/dist/esm/partner/index.d.ts +28 -0
  48. package/dist/esm/partner/index.js +27 -0
  49. package/dist/esm/partner/secret.d.ts +66 -0
  50. package/dist/esm/partner/secret.js +90 -0
  51. package/dist/esm/partner/typen.d.ts +399 -0
  52. package/dist/esm/partner/typen.js +30 -0
  53. package/dist/esm/partner/webhook-signatur.d.ts +95 -0
  54. package/dist/esm/partner/webhook-signatur.js +153 -0
  55. package/dist/esm/partner/webhooks.d.ts +211 -0
  56. package/dist/esm/partner/webhooks.js +257 -0
  57. package/fixtures/hobex-hps-codes.json +1 -1
  58. package/fixtures/oberflaeche.json +131 -3
  59. package/package.json +12 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,154 @@
1
+ # Änderungen
2
+
3
+ Was vor 0.7.0 geschah, steht in der Commit-Historie (`git log`); ab hier wird
4
+ es hier geführt. Ein Eintrag nennt die Änderung **und ihren Grund** —
5
+ nur der Grund überlebt den nächsten Umbau.
6
+
7
+ ## 0.7.0
8
+
9
+ ### Neu: Unterpfad `./partner` — die Partner-API
10
+
11
+ Alles, was ein Partner-Softwarehaus über die Kasseneck-Schnittstelle tut, in
12
+ einem eigenen Unterpfad. Er liegt bewusst **nicht** in der Wurzel: der
13
+ Partner-Schlüssel gehört auf einen Server (er kann Betriebe anlegen und deren
14
+ Geheimnisse holen), und die Kassen-Seite des Pakets soll ihn nicht in ein
15
+ Browser-Bündel ziehen.
16
+
17
+ Neue öffentliche Symbole:
18
+
19
+ - **Anmeldung und Fassade** — `partnerKeyAuth`, `partnerKeyEnv`,
20
+ `createPartnerApi`, `PartnerApi`, `PartnerApiOptions`, `PartnerKeyAuthOptions`.
21
+ - **Betriebe** — `createPartnerCustomer`, `listPartnerCustomers`,
22
+ `getPartnerCustomer`, `sendPartnerCustomerFonLink`, `getPartnerInfo`.
23
+ - **Signatur und Kassen** — `requestCustomerSignature`,
24
+ `getCustomerSignatureStatus`, `createCustomerCashregister`,
25
+ `activateCashregister`, `listCustomerCashregisters`, `getCustomerCredentials`.
26
+ - **Webhooks** — `createPartnerWebhook`, `listPartnerWebhooks`,
27
+ `updatePartnerWebhook`, `deletePartnerWebhook`, `sendPartnerWebhookTest`,
28
+ `listPartnerWebhookDeliveries`, `parseWebhookEvent`,
29
+ `verifyWebhookSignature`, `parseSignatureHeader`, `PARTNER_WEBHOOK_EVENTS`,
30
+ `istPartnerWebhookEvent`, `WEBHOOK_UMSCHLAG_FELDER` und die Konstanten
31
+ `WEBHOOK_SIGNATURE_HEADER`,
32
+ `WEBHOOK_EVENT_HEADER`, `WEBHOOK_DELIVERY_HEADER`, `WEBHOOK_TOLERANCE_SEC`,
33
+ `WEBHOOK_RETRY_PLAN_SEC`, `WEBHOOK_MAX_ATTEMPTS`, `WEBHOOK_TIMEOUT_MS`,
34
+ `WEBHOOK_LIMIT`.
35
+ - **Ablauf und Fehler** — `PARTNER_ABLAUF`, `naechsterSchritt`,
36
+ `PARTNER_FEHLER_CODES`, `PARTNER_PORTAL_FEHLER_CODES`,
37
+ `istPartnerFehlerCode`, `istPartnerPortalFehlerCode`, `istPartnerFehler`,
38
+ `partnerFehlerCode`, `partnerFehlerRat`, `partnerFeldFehler`,
39
+ `partnerWartezeitSek`, `SCOPE_CREDENTIALS`.
40
+ - **Betriebsdaten** — `BETRIEB_FELDER`, `unbekannteBetriebsfelder`,
41
+ `PARTNER_ENVS`.
42
+ - **Geheimnisse** — `KasseneckSecret`, `SECRET_MASKE`.
43
+ - Dazu die Typen der Nutzlasten (`Betrieb`, `Kunde`, `Kasse`, `SignaturAntrag`,
44
+ `CustomerCredentials`, …).
45
+
46
+ Warum die einzelnen Entscheidungen so gefallen sind:
47
+
48
+ - **`KasseneckSecret` statt `string` für die Zugangsdaten eines Betriebs.**
49
+ `getCustomerCredentials` liefert den `api_key` des Betriebs und die Token
50
+ seiner Kassen; wer sie hat, kann in seinem Namen Belege signieren, und ein
51
+ Beleg ist nach RKSV nicht zurücknehmbar. Ein `string` in einem Antwortobjekt
52
+ landet aber in `console.log`, in `JSON.stringify` und im Rumpf eines
53
+ Fehlerberichts. Der Klartext liegt deshalb in einer `WeakMap` neben der
54
+ Instanz — am Objekt hängt kein Feld, das ihn trägt — und `toString`,
55
+ `toJSON`, `Symbol.toPrimitive` sowie der Node-Inspektor zeigen eine Maske.
56
+ Heraus kommt man nur über `.reveal()`; genau diese Stellen findet eine Suche.
57
+ - **Die Signaturprüfung liegt fertig im Paket, nicht als Beispiel in der Doku.**
58
+ Sie ist der Teil, den Integratoren am häufigsten falsch bauen, und ein Fehler
59
+ fällt dort nie auf: eine zu lasche Prüfung lässt jeden durch und meldet nichts.
60
+ Umgesetzt mit WebCrypto statt `node:crypto`, damit das Paket weiterhin ohne
61
+ Laufzeitabhängigkeit auskommt — deshalb ist sie asynchron.
62
+ - **`env` bei `createPartnerCustomer`.** Ohne Angabe entscheidet der Schlüssel.
63
+ Ein **Live**-Schlüssel darf `env:"test"` verlangen — das ist der vorgesehene
64
+ Weg, die ganze Kette zu proben, ohne sich einen zweiten Schlüssel zu holen.
65
+ Umgekehrt nie: ein Test-Schlüssel mit `env:"live"` bekommt `live_not_allowed`,
66
+ und es entsteht nichts. Der Client prüft das **nicht** selbst vor: der Server
67
+ ist die eine Wahrheit, und ein zweiter Torwächter im Paket wäre der, der
68
+ irgendwann veraltet.
69
+ - **Verträge wirken im Partner-Weg nicht mehr** (Backend-Stand 2026-08-31).
70
+ Keine Antwort führt `avv`, `naechsteSchritte` kennt keinen AVV-Schritt, und
71
+ bei der Inbetriebnahme gibt es kein `vertrag_offen` mehr. Weggefallen sind
72
+ deshalb `reportCustomerVertrag`, `vertragOffenRat`, `vertragOffenRatFuer`,
73
+ `AVV_MODI`, `AVV_STATUS`, `avvErfuellt`, `avvSperrt`, `avvStatusText`,
74
+ `istAvvModus`, die Option `avvModus` und der Ablaufschritt `avv`. Der Typ
75
+ `AvvStand` **bleibt** und wird weiterhin gelesen, wenn eine Antwort ihn doch
76
+ führt — vorausgesetzt wird er nirgends. `customer.avv_accepted` steht nicht
77
+ mehr in `PARTNER_WEBHOOK_EVENTS`: es ist ein internes Ereignis, das ein
78
+ Partner weder abonnieren noch proben kann, und ein Name in dieser Liste, den
79
+ niemand bestellen kann, ist ein Versprechen ohne Deckung.
80
+ - **Der Fehlerkatalog ist vollständig** — 28 Codes der Schnittstelle
81
+ (`PARTNER_FEHLER_CODES`) und 12 des Partner-Portals
82
+ (`PARTNER_PORTAL_FEHLER_CODES`), jeder mit Handlungssatz. Quelle ist
83
+ `docs/api/fehlercodes.json` im Backend. Ein Code, den nur eine Seite kennt,
84
+ ist für einen Aufrufer nicht von „gibt es nicht" zu unterscheiden; eine halbe
85
+ Liste ist deshalb schlimmer als keine.
86
+ - **`test: true` im Webhook-Umschlag.** `sendPartnerWebhookTest` nimmt jetzt ein
87
+ `event` und löst damit **jedes abonnierte Ereignis** mit glaubwürdiger
88
+ Nutzlast aus — eine Leitungsprobe beweist nichts über die Behandlung des
89
+ Ernstfalls. Damit eine Probe nicht für echt gehalten wird, trägt sie
90
+ `test: true` im Umschlag; `PartnerWebhookEvent.test` führt das Feld und ist
91
+ bei echten Ereignissen `false`. Die Zeile `if (ereignis.test) return;` gehört
92
+ an den Anfang jedes Handlers — ohne sie schreibt jemand seinem Kunden, die
93
+ Kasse sei fertig.
94
+ - **Betriebsdaten werden streng geprüft.** Das Backend weist ein unbekanntes
95
+ Feld ab, statt es stillschweigend zu verwerfen, und nennt den vollen Pfad
96
+ (`address.land`, `contacts.0.rolle`). Der Typ `Betrieb` führt deshalb genau
97
+ die Liste aus `partner-core.BETRIEB_FELDER`, und `unbekannteBetriebsfelder`
98
+ beantwortet dieselbe Frage zur Laufzeit — für Daten aus Datenbank oder
99
+ Formular, die nie durch die Typprüfung gelaufen sind. Abgewiesen wird hier
100
+ nichts: die Wahrheit bleibt der Server, sonst blockierte ein alter Client ein
101
+ neues Feld.
102
+ - **`createCustomerCashregister` ohne `name`, mit `signaturId`.** Kassennamen
103
+ vergibt Kasseneck (sie sind gleich der `cashregisterId`); ein gesendetes
104
+ `name` wäre ein `validation`-Fehler. Dafür bezieht sich **jede** Kasse auf
105
+ eine Signatur: ohne eine einzige `signature_missing`, bei mehreren
106
+ `signature_ambiguous` ohne `signaturId`. `requestCustomerSignature` nimmt
107
+ `weitere`, um eine zusätzliche Signatur zu beantragen (höchstens zehn,
108
+ `signature_limit`).
109
+ - **`getPartnerInfo().partner.darfZugangEinrichten`** und `zugang.einladen` mit
110
+ Vorgabe **false**: ein Zugang zum Kundenpanel legt einen Login auf eine fremde
111
+ Adresse an und schickt eine Mail dorthin. Fehlt das Feld, gilt NEIN — eine
112
+ Berechtigung, die nicht ausdrücklich dasteht, hat man nicht.
113
+
114
+ ### Geändert: `KasseneckApiError` trägt `code` und `details`
115
+
116
+ Bisher blieb vom `data` einer Fehlerantwort nichts übrig. Die Partner-API legt
117
+ ihre Entscheidung aber nicht in den Text, sondern in `data.code`
118
+ (`live_not_allowed`, `signature_not_ready`, `activation_failed` samt
119
+ `data.schritt`); ohne diese Felder müsste ein Aufrufer die deutsche `message`
120
+ nach Zeichenketten durchsuchen — die Art Kopplung, die beim nächsten
121
+ Formulierungsschliff still bricht.
122
+
123
+ Die Nutzlast wird dabei **gesiebt** (`fehlerDetails`) und nicht durchgereicht:
124
+ höchstens vier Ebenen tief, 50 Einträge je Ebene, Zeichenketten bis 300 Zeichen,
125
+ nur bezeichner-förmige Schlüssel — und kein Wert, der mit einem der gesendeten
126
+ Geheimnisse überlappt. Damit gilt dieselbe Zusage wie für `causeDigest`.
127
+
128
+ Additiv: der dritte Konstruktorparameter hat einen Vorgabewert, bestehende
129
+ Aufrufer und `catch`-Zweige bleiben unverändert.
130
+
131
+ ### Vertrag
132
+
133
+ `AUFRUFE` und damit `fixtures/oberflaeche.json` führen 18 Aufrufe mehr (die
134
+ Partner-Endpunkte). Der Flutter-Zwilling `kasseneck_api` und die
135
+ Hosting-Weiterleitungen in `kasseneck-web` lesen diese Liste — beide müssen
136
+ nachziehen, sonst laufen die Aufrufe in Produktion auf die HTML-Seite statt auf
137
+ die Function.
138
+
139
+ **Neuer Abschnitt `partner` in der Vertragsdatei.** Der Partner-Teil wäre sonst
140
+ als reine Namensliste über den Vertrag gegangen: die Aufrufe hätte er geführt,
141
+ die Fehlercodes, die Webhook-Ereignisse, die Betriebsfelder, die Umgebungen,
142
+ die Felder des Webhook-Umschlags und den Wiederholungsplan nicht — und genau
143
+ die pflegt der Zwilling von Hand nach. Ein Fehlercode, den nur eine Seite kennt,
144
+ hätte auf der anderen keinen Handlungssatz und wäre für einen Aufrufer nicht von
145
+ „gibt es nicht" zu unterscheiden. `scripts/oberflaeche.mjs` liest den
146
+ Partner-Namensraum genauso ab wie den Kassen-Namensraum; eine neu angelegte
147
+ Liste landet damit von selbst im Vertrag statt still zu fehlen.
148
+
149
+ **Und die Gegenrichtung: `test/fixtures/dart-partner.json`.** Der Vertrag in
150
+ `fixtures/` wird drüben geprüft — dieses Repo sähe eine Lücke erst im nächsten
151
+ Zwillingslauf im anderen Repo, an einem anderen Tag. Der eingecheckte Abzug der
152
+ Dart-Seite (dasselbe Muster wie `dart-enums.json`, samt `_quelle`) macht
153
+ `npm test` hier rot, sobald ein Fehlercode, ein Ereignis, ein Betriebsfeld, eine
154
+ Umgebung oder die Marke `test` nur in einer der beiden Sprachen ankommt.
package/README.md CHANGED
@@ -8,7 +8,8 @@ im Test gegengeprüft, damit die beiden Pakete nicht auseinanderdriften.
8
8
  Das Paket deckt ab: Belege ausstellen und stornieren, Belege und Kassen
9
9
  auflisten, Berichte herunterladen, Status bei FinanzOnline abfragen,
10
10
  Stripe-Zahllinks und Hobex-Cloud-Zahlungen, das Beleg-Layout und die
11
- ESC/POS-Erzeugung für den Bondrucker.
11
+ ESC/POS-Erzeugung für den Bondrucker — und unter `./partner` die
12
+ **Partner-API**: Betriebe anlegen und bis zur laufenden Kasse begleiten.
12
13
 
13
14
  **Es läuft im Browser und in Node.** ESM ist das Hauptformat, CommonJS liegt
14
15
  daneben; beides mit eigenen Typdeklarationen. Node ab 20.18 (`fetch` muss
@@ -210,6 +211,123 @@ Grund: „STORNOBELEG / Stornobuchung zu Beleg KASSE1-ID-42 / vom 11.08.2026,
210
211
  09:02 Uhr / Grund: Fehleingabe". Das Datum kommt aus `cancellationOf.timeStamp`
211
212
  (Backend seit 2026-09-04); Altbelege ohne bleiben ohne die Zeile.
212
213
 
214
+ ## Partner-API (`./partner`)
215
+
216
+ Für Softwarehäuser, die Kasseneck in ihr eigenes Produkt einbauen: Betriebe
217
+ anlegen, bis zur laufenden Kasse begleiten und danach in ihrem Namen Belege
218
+ signieren.
219
+
220
+ **Was die Endpunkte tun, steht in der Referenz** —
221
+ `docs/api/partner.md` (ausführlich) und `docs/api/partner.llms.txt` (kompakt,
222
+ für Werkzeuge und Sprachmodelle). Dieses README wiederholt sie nicht; hier
223
+ steht, wie man den Client benutzt.
224
+
225
+ Der Partner-Schlüssel (`pk_live_…`) gehört auf einen **Server**. Er kann
226
+ Betriebe anlegen und — mit dem Zusatz-Scope `credentials:read` — deren
227
+ Geheimnisse holen.
228
+
229
+ ```ts
230
+ import { createPartnerApi, istPartnerFehler } from '@kreiseck/kasseneck-api/partner';
231
+
232
+ const partner = createPartnerApi({ partnerKey: process.env.KASSENECK_PARTNER_KEY! });
233
+
234
+ const { customerId } = await partner.createPartnerCustomer({
235
+ appId: 'app_…',
236
+ idempotencyKey: kundennummer, // die eigene — schützt vor Doppelanlage
237
+ betrieb: { /* Stammdaten, siehe Referenz */ } as never,
238
+ // env: 'test' — auch mit einem LIVE-Schlüssel erlaubt: so probt man die
239
+ // ganze Kette, ohne sich einen zweiten Schlüssel zu holen. Umgekehrt nie.
240
+ });
241
+
242
+ await partner.sendPartnerCustomerFonLink(customerId);
243
+ // … auf das Ereignis customer.fon_verified warten …
244
+ await partner.requestCustomerSignature(customerId);
245
+ // … auf signature.ready warten …
246
+ await partner.createCustomerCashregister({ customerId }); // automatisch:true ist Vorgabe
247
+ ```
248
+
249
+ Die Reihenfolge ist hart, und jeder Schritt beschwert sich mit einem eigenen
250
+ Code, wenn ein vorheriger fehlt. Sie steht als Daten im Paket
251
+ (`PARTNER_ABLAUF`), und zu jedem Code gibt es einen Handlungssatz:
252
+
253
+ ```ts
254
+ try {
255
+ await partner.activateCashregister(customerId, cashregisterId);
256
+ } catch (fehler) {
257
+ if (istPartnerFehler(fehler, 'signature_not_ready')) {
258
+ // Die Signatur DIESER Kasse ist noch nicht bereit — auf signature.ready warten.
259
+ console.error(partner.fehlerRat('signature_not_ready'));
260
+ }
261
+ }
262
+ ```
263
+
264
+ ### Eine Probe ist keine Kasse
265
+
266
+ `sendPartnerWebhookTest(webhookId, 'cashregister.live')` löst genau das
267
+ Ereignis aus, das der eigene Handler behandeln soll — eine Leitungsprobe
268
+ beweist nichts über die Behandlung des Ernstfalls. Damit niemand eine Probe
269
+ für echt hält, trägt sie `test: true` im Umschlag:
270
+
271
+ ```ts
272
+ const geprueft = await parseWebhookEvent({ secret, signatureHeader, body, });
273
+ if (!geprueft.ok) return antwort(400);
274
+
275
+ if (geprueft.event.test) return antwort(200); // Probe: nichts weiter tun
276
+ ```
277
+
278
+ Ohne diese Zeile schreibt jemand seinem Kunden, die Kasse sei fertig.
279
+
280
+ ### Zugangsdaten sind Geheimnisse eines Dritten
281
+
282
+ `getCustomerCredentials` liefert den `api_key` des Betriebs und die Token
283
+ seiner Kassen. Wer sie hat, kann in seinem Namen Belege signieren — und ein
284
+ Beleg ist nach RKSV nicht zurücknehmbar. Sie kommen deshalb **nicht als
285
+ `string`**, sondern in einer Hülle, die sich nicht versehentlich ausgeben
286
+ lässt:
287
+
288
+ ```ts
289
+ const zugang = await partner.getCustomerCredentials(customerId);
290
+
291
+ console.log(zugang); // [apiKey «verborgen»] — kein Klartext
292
+ JSON.stringify(zugang); // ebenso
293
+ `${zugang.apiKey}`; // ebenso
294
+
295
+ speichereVerschluesselt(zugang.apiKey.reveal()); // der einzige Weg heraus
296
+ ```
297
+
298
+ Nur verschlüsselt speichern, nie protokollieren, nie in eine Mail oder einen
299
+ Fehlerbericht. Jeder Abruf wird mitgeschrieben und ist für den Betrieb
300
+ sichtbar.
301
+
302
+ ### Eingehende Webhooks prüfen
303
+
304
+ Das ist die Stelle, an der Integrationen am häufigsten scheitern — deshalb
305
+ liegt sie fertig im Paket. Vier Dinge müssen stimmen: der **rohe** Rumpf, das
306
+ Zeitfenster gegen Wiedereinspielung, ein zeitkonstanter Vergleich, und jede
307
+ Ausnahme als Ablehnung.
308
+
309
+ ```ts
310
+ import express from 'express';
311
+ import { parseWebhookEvent } from '@kreiseck/kasseneck-api/partner';
312
+
313
+ const app = express();
314
+
315
+ // express.raw VOR jedem JSON-Parser: signiert sind die Bytes, die ankommen.
316
+ app.post('/kasseneck-webhook', express.raw({ type: '*/*' }), async (req, res) => {
317
+ const ergebnis = await parseWebhookEvent({
318
+ secret: process.env.KASSENECK_WEBHOOK_SECRET!,
319
+ signatureHeader: req.header('X-Kasseneck-Signature'),
320
+ body: req.body, // Buffer — nicht req.body nach JSON.parse
321
+ });
322
+ if (!ergebnis.ok) return res.status(400).send(ergebnis.reason);
323
+
324
+ // Innerhalb von 10 s antworten, Arbeit danach. Zustellungen können sich
325
+ // wiederholen: auf event.id entdoppeln.
326
+ res.sendStatus(200);
327
+ await verarbeite(ergebnis.event);
328
+ });
329
+ ```
330
+
213
331
  ## Unterpfade
214
332
 
215
333
  | Unterpfad | Inhalt |
@@ -220,6 +338,7 @@ Grund: „STORNOBELEG / Stornobuchung zu Beleg KASSE1-ID-42 / vom 11.08.2026,
220
338
  | `…/payments` | Stripe-Zahllinks, Hobex-Cloud (beides HTTP-Endpunkte des Backends) und Hobex **HPS** über **Kasseneck Connect** (lokaler Geräte-Agent, spricht mit dem Terminal). |
221
339
  | `…/register` | Anmeldung der Browser-Kasse: Gerät koppeln und entkoppeln, Benutzer auflisten, per PIN anmelden, Sitzung erneuern und beenden. |
222
340
  | `…/kasse` | Kachel-Kasse: Kassen-Einstellungen (betriebsweit / je Gerät), Artikelgruppen und Artikel für Kacheln, Rabattverteilung je Steuersatz, Reichweiten der Kassen-Rechte |
341
+ | `…/partner` | Partner-API: Betriebe anlegen, FinanzOnline-Link, Signatur, Kassen, Zugangsdaten, Webhooks samt Signaturprüfung. **Gehört auf einen Server.** |
223
342
  | `…/react` | Dünner React-Adapter, der ein Beleg-Layout zeichnet. Braucht React. |
224
343
  | `…/fixtures/*` | Golden-Belege (JSON): Eingaben `belege/<name>.json`, zugesagte Zeilenausgabe `erwartet/<name>.lines.json`, `manifest.json` mit Prüfsummen — dieselben Dateien prüfen Backend, Browser-Kasse und Flutter-Paket. |
225
344
 
@@ -305,6 +424,8 @@ im Tarball mit und sagen in Maschinenform, worauf sich beide Seiten geeinigt hab
305
424
  | `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen |
306
425
  | `hobex-hps-codes.json` | Gemessene HPS-Ergebniscodes, ihre Bedeutung und ob sie einen Ausgang festschreiben — der Vertrag hinter `.../payments/hobex-hps`s `isConclusive`. |
307
426
 
427
+ | `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen, Partner-Listen |
428
+
308
429
  Alle drei werden erzeugt (`npm run fixtures:kasse`, `npm run fixtures:oberflaeche`,
309
430
  `npm run fixtures:hobex-hps-codes`) und nie von Hand geändert; die CI prüft
310
431
  nach jedem Lauf, dass sie zum Code passen.
@@ -313,6 +434,22 @@ nach jedem Lauf, dass sie zum Code passen.
313
434
  jedem `npm version` müssen deshalb alle drei Dateien neu erzeugt und
314
435
  mitcommittet werden**, sonst wird die CI rot.
315
436
 
437
+ ### Und die Gegenrichtung
438
+
439
+ Der Vertrag in `fixtures/` wird **drüben** geprüft: das Dart-Repo zieht ihn und
440
+ hält seine Listen dagegen. Eine Lücke fiele hier deshalb erst im nächsten
441
+ Zwillingslauf im anderen Repo auf — an einem anderen Tag. Dagegen stehen zwei
442
+ von Hand gepflegte Abzüge der Dart-Seite unter `test/fixtures/`, jeder mit
443
+ `_quelle`:
444
+
445
+ | Datei | prüft |
446
+ |---|---|
447
+ | `dart-enums.json` | Belegtyp, Steuersatz, Zahlungsart, Kartenanbieter, Gutschein, Stripe-Modus |
448
+ | `dart-partner.json` | Umgebungen, Fehlercodes (API und Portal), Webhook-Ereignisse, Felder des Webhook-Umschlags samt der Marke `test`, Betriebsfelder, Wiederholungsplan |
449
+
450
+ Sie machen `npm test` rot, sobald ein Wert nur in einer der beiden Sprachen
451
+ ankommt.
452
+
316
453
  ## Lizenz
317
454
 
318
455
  Apache-2.0 — siehe `LICENSE` und `NOTICE`.
@@ -13,7 +13,7 @@
13
13
  * umhuellt, muss ihn weiterhin absetzen koennen.
14
14
  */
15
15
  import type { TransportBodyFields } from './transport.js';
16
- export declare const AUFRUFE: readonly ["cancelReceipt", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getFirstReceiptDate", "getKasseSettings", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listRegisterUsersForDevice", "listRegisterSessionsForDevice", "pairRegisterDevice", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice"];
16
+ export declare const AUFRUFE: readonly ["activateCashregister", "cancelReceipt", "createCustomerCashregister", "createPartnerCustomer", "checkPartnerCustomerEmail", "createPartnerWebhook", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "deletePartnerWebhook", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getCustomerCredentials", "getCustomerSignatureStatus", "getFirstReceiptDate", "getKasseSettings", "getPartnerCustomer", "getPartnerInfo", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "listCustomerCashregisters", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listPartnerCustomers", "listPartnerWebhookDeliveries", "listPartnerWebhooks", "listRegisterUsersForDevice", "listRegisterSessionsForDevice", "pairRegisterDevice", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "requestCustomerSignature", "sendPartnerCustomerFonLink", "sendPartnerWebhookTest", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice", "rotatePartnerWebhookSecret", "updatePartnerWebhook"];
17
17
  export type Aufruf = typeof AUFRUFE[number];
18
18
  /** Wie [KasseneckTransport], nur mit bekanntem Aufrufnamen. Nicht exportiert nach aussen. */
19
19
  export type InternerTransport = <T = unknown>(functionName: Aufruf, params?: Record<string, unknown>, extraBodyFields?: TransportBodyFields, secretParams?: readonly string[]) => Promise<T>;
@@ -2,35 +2,54 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AUFRUFE = void 0;
4
4
  exports.AUFRUFE = [
5
+ 'activateCashregister',
5
6
  'cancelReceipt',
7
+ 'createCustomerCashregister',
8
+ 'createPartnerCustomer',
9
+ 'checkPartnerCustomerEmail',
10
+ 'createPartnerWebhook',
6
11
  'createPaymentLinkStripe',
7
12
  'createPrintJob',
8
13
  'createReceipt',
14
+ 'deletePartnerWebhook',
9
15
  'downloadDailyReport',
10
16
  'downloadReport',
11
17
  'endRegisterSession',
12
18
  'financeWebService',
13
19
  'generateFullReceiptId',
20
+ 'getCustomerCredentials',
21
+ 'getCustomerSignatureStatus',
14
22
  'getFirstReceiptDate',
15
23
  'getKasseSettings',
24
+ 'getPartnerCustomer',
25
+ 'getPartnerInfo',
16
26
  'getPrintJob',
17
27
  'getReceipt',
18
28
  'hobexPayApi',
19
29
  'hobexRefundApi',
30
+ 'listCustomerCashregisters',
20
31
  'listMyArticleGroups',
21
32
  'listMyArticles',
22
33
  'listMyCashregisters',
23
34
  'listMyPrinters',
24
35
  'listMyReceipts',
25
36
  'listMyTipRecipients',
37
+ 'listPartnerCustomers',
38
+ 'listPartnerWebhookDeliveries',
39
+ 'listPartnerWebhooks',
26
40
  'listRegisterUsersForDevice',
27
41
  'listRegisterSessionsForDevice',
28
42
  'pairRegisterDevice',
29
43
  'registerPinLogin',
30
44
  'registerUserLogin',
31
45
  'renewRegisterSession',
46
+ 'requestCustomerSignature',
47
+ 'sendPartnerCustomerFonLink',
48
+ 'sendPartnerWebhookTest',
32
49
  'setMyKasseSettings',
33
50
  'setMyRegisterDeviceSettings',
34
51
  'stripeCaptureIntent',
35
52
  'unpairRegisterDevice',
53
+ 'rotatePartnerWebhookSecret',
54
+ 'updatePartnerWebhook',
36
55
  ];
@@ -69,6 +69,24 @@ export interface CauseDigest {
69
69
  * gesendeten Werte nicht bekannt sind, wird gar nicht verdichtet.
70
70
  */
71
71
  export declare function causeDigest(ursache: unknown, geheimnisse: readonly string[]): CauseDigest;
72
+ /**
73
+ * Siebt das `data` einer Fehlerantwort zu einer Form, die man gefahrlos an
74
+ * einem Fehler mitfuehren kann.
75
+ *
76
+ * **Warum ueberhaupt:** die Partner-API legt ihre Entscheidung nicht in den
77
+ * Text, sondern in `data.code` (`vertrag_offen`, `signature_not_ready`,
78
+ * `activation_failed` samt `data.schritt`). Ohne diese Felder muesste ein
79
+ * Aufrufer die deutsche `message` nach Zeichenketten durchsuchen — genau die
80
+ * Kopplung, die beim naechsten Formulierungsschliff still bricht.
81
+ *
82
+ * **Warum gesiebt und nicht durchgereicht:** siehe Modulkommentar. Der Rumpf
83
+ * kommt ueber fremde Proxys, und ein Fehler landet in Protokollen. Deshalb
84
+ * ueberlebt nur, was flach, klein und bezeichner-foermig benannt ist — und
85
+ * kein Wert, der mit einem der gesendeten Geheimnisse ueberlappt. Damit gilt
86
+ * hier dieselbe Zusage wie fuer [causeDigest], und `geheimnisse` hat aus
87
+ * demselben Grund **keinen** Vorgabewert.
88
+ */
89
+ export declare function fehlerDetails(daten: unknown, geheimnisse: readonly string[]): Record<string, unknown>;
72
90
  /** Fachlicher Fehler: HTTP 200, aber `status: 'error'` im Rumpf. */
73
91
  export declare class KasseneckApiError extends Error {
74
92
  readonly name = "KasseneckApiError";
@@ -77,12 +95,22 @@ export declare class KasseneckApiError extends Error {
77
95
  /** Meldung des Backends, unveraendert (`message` aus der Huelle). */
78
96
  readonly serverMessage: string;
79
97
  /**
80
- * Stabiler Fehlercode des Backends (`code` aus der Huelle), wenn der
81
- * Endpunkt einen legt heute `cancelReceipt` (siehe CANCELLATION_ERROR_CODES).
82
- * Daran entscheiden, nie an [serverMessage]: der Text darf sich aendern.
98
+ * Stabiler, maschinenlesbarer Fehlercode des Backends, wenn der Endpunkt
99
+ * einen legt. Zwei Ablageorte kommen vor und beide zaehlen: das Feld `code`
100
+ * neben `message` (cancelReceipt, siehe CANCELLATION_ERROR_CODES) und
101
+ * `data.code` (Partner-API: `vertrag_offen`, `rate_limited`, …). Nur
102
+ * Bezeichner gelten — ein Freitext waere kein Code. Die aelteren Endpunkte
103
+ * antworten ohne Code — dann `undefined`. **Daran entscheiden, nie an
104
+ * [serverMessage]:** der Text darf sich aendern, der Code nicht.
83
105
  */
84
106
  readonly code: string | undefined;
85
- constructor(functionName: string, serverMessage: string, code?: string);
107
+ /**
108
+ * Die uebrige Fehler-Nutzlast, gesiebt (siehe [fehlerDetails]): `schritt`,
109
+ * `rc`, `retryAfterSec`, `errors[]` und was der jeweilige Endpunkt sonst
110
+ * beilegt. Immer ein Objekt, notfalls ein leeres.
111
+ */
112
+ readonly details: Record<string, unknown>;
113
+ constructor(functionName: string, serverMessage: string, details?: Record<string, unknown>, code?: string);
86
114
  }
87
115
  /** Warum die Antwort keine verwertbare Huelle war. */
88
116
  export type HttpFailureReason = 'server-error' | 'empty-body' | 'not-json' | 'missing-status';
@@ -58,6 +58,7 @@
58
58
  Object.defineProperty(exports, "__esModule", { value: true });
59
59
  exports.KasseneckValidationError = exports.KasseneckAuthError = exports.KasseneckNetworkError = exports.KasseneckHttpError = exports.KasseneckApiError = void 0;
60
60
  exports.causeDigest = causeDigest;
61
+ exports.fehlerDetails = fehlerDetails;
61
62
  exports.isKasseneckApiError = isKasseneckApiError;
62
63
  exports.isKasseneckHttpError = isKasseneckHttpError;
63
64
  exports.isKasseneckNetworkError = isKasseneckNetworkError;
@@ -94,6 +95,84 @@ function unbedenklich(wert, geheimnisse) {
94
95
  }
95
96
  return wert;
96
97
  }
98
+ /**
99
+ * Grenzen fuer die gesiebte Fehler-Nutzlast (siehe [fehlerDetails]). Sie stehen
100
+ * als benannte Konstanten hier, weil ein Test sie namentlich prueft — eine
101
+ * spaeter heraufgesetzte Grenze soll auffallen und nicht als Zahl im Code
102
+ * untergehen.
103
+ */
104
+ const DETAIL_TIEFE = 4;
105
+ const DETAIL_EINTRAEGE = 50;
106
+ const DETAIL_TEXT_MAX = 300;
107
+ /** Schluessel eines Detail-Objekts: bezeichner-foermig, sonst faellt der Eintrag weg. */
108
+ const DETAIL_SCHLUESSEL = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
109
+ /**
110
+ * Siebt das `data` einer Fehlerantwort zu einer Form, die man gefahrlos an
111
+ * einem Fehler mitfuehren kann.
112
+ *
113
+ * **Warum ueberhaupt:** die Partner-API legt ihre Entscheidung nicht in den
114
+ * Text, sondern in `data.code` (`vertrag_offen`, `signature_not_ready`,
115
+ * `activation_failed` samt `data.schritt`). Ohne diese Felder muesste ein
116
+ * Aufrufer die deutsche `message` nach Zeichenketten durchsuchen — genau die
117
+ * Kopplung, die beim naechsten Formulierungsschliff still bricht.
118
+ *
119
+ * **Warum gesiebt und nicht durchgereicht:** siehe Modulkommentar. Der Rumpf
120
+ * kommt ueber fremde Proxys, und ein Fehler landet in Protokollen. Deshalb
121
+ * ueberlebt nur, was flach, klein und bezeichner-foermig benannt ist — und
122
+ * kein Wert, der mit einem der gesendeten Geheimnisse ueberlappt. Damit gilt
123
+ * hier dieselbe Zusage wie fuer [causeDigest], und `geheimnisse` hat aus
124
+ * demselben Grund **keinen** Vorgabewert.
125
+ */
126
+ function fehlerDetails(daten, geheimnisse) {
127
+ const gesiebt = sieben(daten, geheimnisse, 0);
128
+ return gesiebt !== null && typeof gesiebt === 'object' && !Array.isArray(gesiebt)
129
+ ? gesiebt
130
+ : {};
131
+ }
132
+ function sieben(wert, geheimnisse, tiefe) {
133
+ if (wert === null || typeof wert === 'boolean')
134
+ return wert;
135
+ // Nur endliche Zahlen: NaN und Infinity ueberstehen JSON.stringify nicht und
136
+ // staenden in einem Fehlerbericht als `null` ohne jede Aussage.
137
+ if (typeof wert === 'number')
138
+ return Number.isFinite(wert) ? wert : undefined;
139
+ if (typeof wert === 'string') {
140
+ if (wert.length > DETAIL_TEXT_MAX)
141
+ return undefined;
142
+ for (const geheim of geheimnisse) {
143
+ if (geheim && (geheim.includes(wert) || wert.includes(geheim)))
144
+ return undefined;
145
+ }
146
+ return wert;
147
+ }
148
+ if (tiefe >= DETAIL_TIEFE)
149
+ return undefined;
150
+ if (Array.isArray(wert)) {
151
+ const liste = [];
152
+ for (const eintrag of wert.slice(0, DETAIL_EINTRAEGE)) {
153
+ const s = sieben(eintrag, geheimnisse, tiefe + 1);
154
+ if (s !== undefined)
155
+ liste.push(s);
156
+ }
157
+ return liste;
158
+ }
159
+ if (typeof wert !== 'object')
160
+ return undefined;
161
+ const raus = {};
162
+ let gezaehlt = 0;
163
+ for (const [schluessel, eintrag] of Object.entries(wert)) {
164
+ if (gezaehlt >= DETAIL_EINTRAEGE)
165
+ break;
166
+ if (!DETAIL_SCHLUESSEL.test(schluessel))
167
+ continue;
168
+ const s = sieben(eintrag, geheimnisse, tiefe + 1);
169
+ if (s === undefined)
170
+ continue;
171
+ raus[schluessel] = s;
172
+ gezaehlt += 1;
173
+ }
174
+ return raus;
175
+ }
97
176
  /** Fachlicher Fehler: HTTP 200, aber `status: 'error'` im Rumpf. */
98
177
  class KasseneckApiError extends Error {
99
178
  name = 'KasseneckApiError';
@@ -102,16 +181,30 @@ class KasseneckApiError extends Error {
102
181
  /** Meldung des Backends, unveraendert (`message` aus der Huelle). */
103
182
  serverMessage;
104
183
  /**
105
- * Stabiler Fehlercode des Backends (`code` aus der Huelle), wenn der
106
- * Endpunkt einen legt heute `cancelReceipt` (siehe CANCELLATION_ERROR_CODES).
107
- * Daran entscheiden, nie an [serverMessage]: der Text darf sich aendern.
184
+ * Stabiler, maschinenlesbarer Fehlercode des Backends, wenn der Endpunkt
185
+ * einen legt. Zwei Ablageorte kommen vor und beide zaehlen: das Feld `code`
186
+ * neben `message` (cancelReceipt, siehe CANCELLATION_ERROR_CODES) und
187
+ * `data.code` (Partner-API: `vertrag_offen`, `rate_limited`, …). Nur
188
+ * Bezeichner gelten — ein Freitext waere kein Code. Die aelteren Endpunkte
189
+ * antworten ohne Code — dann `undefined`. **Daran entscheiden, nie an
190
+ * [serverMessage]:** der Text darf sich aendern, der Code nicht.
108
191
  */
109
192
  code;
110
- constructor(functionName, serverMessage, code) {
193
+ /**
194
+ * Die uebrige Fehler-Nutzlast, gesiebt (siehe [fehlerDetails]): `schritt`,
195
+ * `rc`, `retryAfterSec`, `errors[]` und was der jeweilige Endpunkt sonst
196
+ * beilegt. Immer ein Objekt, notfalls ein leeres.
197
+ */
198
+ details;
199
+ constructor(functionName, serverMessage, details = {}, code) {
111
200
  super(`${functionName} fehlgeschlagen: ${serverMessage}`);
112
201
  this.functionName = functionName;
113
202
  this.serverMessage = serverMessage;
114
- this.code = code;
203
+ this.details = details;
204
+ // Der Code neben `message` hat Vorrang; fehlt er, gilt `data.code` aus
205
+ // den Details. So bleibt die Klasse fuer beide Ablageorte dieselbe.
206
+ const kandidat = code !== undefined ? code : details['code'];
207
+ this.code = typeof kandidat === 'string' && BEZEICHNER.test(kandidat) ? kandidat : undefined;
115
208
  }
116
209
  }
117
210
  exports.KasseneckApiError = KasseneckApiError;
@@ -134,7 +134,7 @@ function createCore(options) {
134
134
  if (antwort.status !== 200) {
135
135
  throw new errors_js_1.KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'server-error');
136
136
  }
137
- return auswerten(koerper, fehlerName, antwort.status, inhaltstyp);
137
+ return auswerten(koerper, fehlerName, antwort.status, inhaltstyp, geheimnisse);
138
138
  }
139
139
  finally {
140
140
  // Ohne Abraeumen haelt der Wecker den Node-Prozess bis zum Zeitlimit wach.
@@ -159,7 +159,7 @@ const alsBytes = async (antwort, functionName) => {
159
159
  return new Uint8Array(await antwort.arrayBuffer());
160
160
  };
161
161
  /** Auswertung des JSON-Wegs: Huelle aufloesen, Nutzlast zurueckgeben. */
162
- function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
162
+ function jsonAuswerten(text, functionName, statusCode, inhaltstyp, geheimnisse) {
163
163
  if (!text.trim()) {
164
164
  throw new errors_js_1.KasseneckHttpError(functionName, statusCode, inhaltstyp, 'empty-body');
165
165
  }
@@ -181,7 +181,7 @@ function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
181
181
  }
182
182
  // Alles, was nicht ausdruecklich Erfolg ist, gilt als fachlicher Fehler —
183
183
  // ein unbekannter Statuswert darf nie stillschweigend als Erfolg durchgehen.
184
- throw fachfehler(functionName, huelle.message, huelle.code);
184
+ throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse, huelle.code);
185
185
  }
186
186
  /**
187
187
  * Auswertung des Binaerwegs. Der Kern der Zusage: **ein Aufrufer bekommt nie
@@ -196,7 +196,7 @@ function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
196
196
  * das Backend hier erzeugt (pdf-lib). Alles, was nicht so anfaengt, ist kein
197
197
  * Bericht — und wird dann daraufhin angesehen, ob es die Fehlerhuelle ist.
198
198
  */
199
- function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp) {
199
+ function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp, geheimnisse) {
200
200
  if (bytes.length === 0) {
201
201
  throw new errors_js_1.KasseneckHttpError(functionName, statusCode, inhaltstyp, 'empty-body');
202
202
  }
@@ -227,7 +227,7 @@ function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp) {
227
227
  }
228
228
  // Derselbe fachliche Fehler wie auf dem JSON-Weg — fuer den Aufrufer macht
229
229
  // es keinen Unterschied, ob er ein PDF oder eine Nutzlast erwartet hat.
230
- throw fachfehler(functionName, huelle.message, huelle.code);
230
+ throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse, huelle.code);
231
231
  }
232
232
  /** `%PDF` am Anfang — die Kennung jeder PDF-Datei. */
233
233
  function istPdf(bytes) {
@@ -240,10 +240,10 @@ function alsHuelle(wert) {
240
240
  }
241
241
  return wert;
242
242
  }
243
- function fachfehler(functionName, message, code) {
243
+ function fachfehler(functionName, message, daten, geheimnisse, code) {
244
244
  const meldung = typeof message === 'string' && message.trim() ? message : 'Unbekannter Fehler';
245
245
  // Nur ein Text zaehlt als Code -- alles andere waere ein geratener Vertrag.
246
- return new errors_js_1.KasseneckApiError(functionName, meldung, typeof code === 'string' && code ? code : undefined);
246
+ return new errors_js_1.KasseneckApiError(functionName, meldung, (0, errors_js_1.fehlerDetails)(daten, geheimnisse), typeof code === 'string' && code ? code : undefined);
247
247
  }
248
248
  /**
249
249
  * Nutzlast aus Auth- und Aufruferparametern. Auth-Parameter bilden die