@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.
- package/CHANGELOG.md +154 -0
- package/README.md +138 -1
- package/dist/cjs/client/aufrufe.d.ts +1 -1
- package/dist/cjs/client/aufrufe.js +19 -0
- package/dist/cjs/client/errors.d.ts +32 -4
- package/dist/cjs/client/errors.js +98 -5
- package/dist/cjs/client/transport.js +7 -7
- package/dist/cjs/partner/ablauf.d.ts +47 -0
- package/dist/cjs/partner/ablauf.js +99 -0
- package/dist/cjs/partner/api.d.ts +68 -0
- package/dist/cjs/partner/api.js +44 -0
- package/dist/cjs/partner/auth.d.ts +33 -0
- package/dist/cjs/partner/auth.js +52 -0
- package/dist/cjs/partner/betrieb.d.ts +48 -0
- package/dist/cjs/partner/betrieb.js +136 -0
- package/dist/cjs/partner/endpunkte.d.ts +142 -0
- package/dist/cjs/partner/endpunkte.js +488 -0
- package/dist/cjs/partner/fehler.d.ts +72 -0
- package/dist/cjs/partner/fehler.js +199 -0
- package/dist/cjs/partner/index.d.ts +28 -0
- package/dist/cjs/partner/index.js +84 -0
- package/dist/cjs/partner/secret.d.ts +66 -0
- package/dist/cjs/partner/secret.js +95 -0
- package/dist/cjs/partner/typen.d.ts +399 -0
- package/dist/cjs/partner/typen.js +33 -0
- package/dist/cjs/partner/webhook-signatur.d.ts +95 -0
- package/dist/cjs/partner/webhook-signatur.js +158 -0
- package/dist/cjs/partner/webhooks.d.ts +211 -0
- package/dist/cjs/partner/webhooks.js +269 -0
- package/dist/esm/client/aufrufe.d.ts +1 -1
- package/dist/esm/client/aufrufe.js +19 -0
- package/dist/esm/client/errors.d.ts +32 -4
- package/dist/esm/client/errors.js +97 -5
- package/dist/esm/client/transport.js +8 -8
- package/dist/esm/partner/ablauf.d.ts +47 -0
- package/dist/esm/partner/ablauf.js +95 -0
- package/dist/esm/partner/api.d.ts +68 -0
- package/dist/esm/partner/api.js +41 -0
- package/dist/esm/partner/auth.d.ts +33 -0
- package/dist/esm/partner/auth.js +48 -0
- package/dist/esm/partner/betrieb.d.ts +48 -0
- package/dist/esm/partner/betrieb.js +132 -0
- package/dist/esm/partner/endpunkte.d.ts +142 -0
- package/dist/esm/partner/endpunkte.js +474 -0
- package/dist/esm/partner/fehler.d.ts +72 -0
- package/dist/esm/partner/fehler.js +189 -0
- package/dist/esm/partner/index.d.ts +28 -0
- package/dist/esm/partner/index.js +27 -0
- package/dist/esm/partner/secret.d.ts +66 -0
- package/dist/esm/partner/secret.js +90 -0
- package/dist/esm/partner/typen.d.ts +399 -0
- package/dist/esm/partner/typen.js +30 -0
- package/dist/esm/partner/webhook-signatur.d.ts +95 -0
- package/dist/esm/partner/webhook-signatur.js +153 -0
- package/dist/esm/partner/webhooks.d.ts +211 -0
- package/dist/esm/partner/webhooks.js +257 -0
- package/fixtures/hobex-hps-codes.json +1 -1
- package/fixtures/oberflaeche.json +131 -3
- 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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
-
|
|
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
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|