@kreiseck/kasseneck-api 0.6.45 → 0.7.0
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 +158 -2
- package/dist/cjs/client/aufrufe.d.ts +1 -1
- package/dist/cjs/client/aufrufe.js +20 -0
- package/dist/cjs/client/errors.d.ts +31 -1
- package/dist/cjs/client/errors.js +98 -1
- package/dist/cjs/client/transport.js +7 -7
- package/dist/cjs/kasse/index.d.ts +1 -0
- package/dist/cjs/kasse/index.js +3 -0
- package/dist/cjs/kasse/trinkgeld.d.ts +10 -0
- package/dist/cjs/kasse/trinkgeld.js +27 -0
- 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/cjs/register/pairing.d.ts +8 -1
- package/dist/cjs/register/pairing.js +8 -1
- package/dist/esm/client/aufrufe.d.ts +1 -1
- package/dist/esm/client/aufrufe.js +20 -0
- package/dist/esm/client/errors.d.ts +31 -1
- package/dist/esm/client/errors.js +97 -1
- package/dist/esm/client/transport.js +8 -8
- package/dist/esm/kasse/index.d.ts +1 -0
- package/dist/esm/kasse/index.js +1 -0
- package/dist/esm/kasse/trinkgeld.d.ts +10 -0
- package/dist/esm/kasse/trinkgeld.js +24 -0
- 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/dist/esm/register/pairing.d.ts +8 -1
- package/dist/esm/register/pairing.js +8 -1
- package/fixtures/oberflaeche.json +132 -3
- package/package.json +13 -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
|
|
@@ -167,6 +168,123 @@ sich nicht stornieren, ein voll stornierter Beleg nicht noch einmal. Am
|
|
|
167
168
|
gelesenen Original liefert `remainingQuantities(receipt)` die Reste vorab (für
|
|
168
169
|
den Storno-Dialog); die Wahrheit hat der Server.
|
|
169
170
|
|
|
171
|
+
## Partner-API (`./partner`)
|
|
172
|
+
|
|
173
|
+
Für Softwarehäuser, die Kasseneck in ihr eigenes Produkt einbauen: Betriebe
|
|
174
|
+
anlegen, bis zur laufenden Kasse begleiten und danach in ihrem Namen Belege
|
|
175
|
+
signieren.
|
|
176
|
+
|
|
177
|
+
**Was die Endpunkte tun, steht in der Referenz** —
|
|
178
|
+
`docs/api/partner.md` (ausführlich) und `docs/api/partner.llms.txt` (kompakt,
|
|
179
|
+
für Werkzeuge und Sprachmodelle). Dieses README wiederholt sie nicht; hier
|
|
180
|
+
steht, wie man den Client benutzt.
|
|
181
|
+
|
|
182
|
+
Der Partner-Schlüssel (`pk_live_…`) gehört auf einen **Server**. Er kann
|
|
183
|
+
Betriebe anlegen und — mit dem Zusatz-Scope `credentials:read` — deren
|
|
184
|
+
Geheimnisse holen.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { createPartnerApi, istPartnerFehler } from '@kreiseck/kasseneck-api/partner';
|
|
188
|
+
|
|
189
|
+
const partner = createPartnerApi({ partnerKey: process.env.KASSENECK_PARTNER_KEY! });
|
|
190
|
+
|
|
191
|
+
const { customerId } = await partner.createPartnerCustomer({
|
|
192
|
+
appId: 'app_…',
|
|
193
|
+
idempotencyKey: kundennummer, // die eigene — schützt vor Doppelanlage
|
|
194
|
+
betrieb: { /* Stammdaten, siehe Referenz */ } as never,
|
|
195
|
+
// env: 'test' — auch mit einem LIVE-Schlüssel erlaubt: so probt man die
|
|
196
|
+
// ganze Kette, ohne sich einen zweiten Schlüssel zu holen. Umgekehrt nie.
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await partner.sendPartnerCustomerFonLink(customerId);
|
|
200
|
+
// … auf das Ereignis customer.fon_verified warten …
|
|
201
|
+
await partner.requestCustomerSignature(customerId);
|
|
202
|
+
// … auf signature.ready warten …
|
|
203
|
+
await partner.createCustomerCashregister({ customerId }); // automatisch:true ist Vorgabe
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Die Reihenfolge ist hart, und jeder Schritt beschwert sich mit einem eigenen
|
|
207
|
+
Code, wenn ein vorheriger fehlt. Sie steht als Daten im Paket
|
|
208
|
+
(`PARTNER_ABLAUF`), und zu jedem Code gibt es einen Handlungssatz:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
try {
|
|
212
|
+
await partner.activateCashregister(customerId, cashregisterId);
|
|
213
|
+
} catch (fehler) {
|
|
214
|
+
if (istPartnerFehler(fehler, 'signature_not_ready')) {
|
|
215
|
+
// Die Signatur DIESER Kasse ist noch nicht bereit — auf signature.ready warten.
|
|
216
|
+
console.error(partner.fehlerRat('signature_not_ready'));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Eine Probe ist keine Kasse
|
|
222
|
+
|
|
223
|
+
`sendPartnerWebhookTest(webhookId, 'cashregister.live')` löst genau das
|
|
224
|
+
Ereignis aus, das der eigene Handler behandeln soll — eine Leitungsprobe
|
|
225
|
+
beweist nichts über die Behandlung des Ernstfalls. Damit niemand eine Probe
|
|
226
|
+
für echt hält, trägt sie `test: true` im Umschlag:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const geprueft = await parseWebhookEvent({ secret, signatureHeader, body, });
|
|
230
|
+
if (!geprueft.ok) return antwort(400);
|
|
231
|
+
|
|
232
|
+
if (geprueft.event.test) return antwort(200); // Probe: nichts weiter tun
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Ohne diese Zeile schreibt jemand seinem Kunden, die Kasse sei fertig.
|
|
236
|
+
|
|
237
|
+
### Zugangsdaten sind Geheimnisse eines Dritten
|
|
238
|
+
|
|
239
|
+
`getCustomerCredentials` liefert den `api_key` des Betriebs und die Token
|
|
240
|
+
seiner Kassen. Wer sie hat, kann in seinem Namen Belege signieren — und ein
|
|
241
|
+
Beleg ist nach RKSV nicht zurücknehmbar. Sie kommen deshalb **nicht als
|
|
242
|
+
`string`**, sondern in einer Hülle, die sich nicht versehentlich ausgeben
|
|
243
|
+
lässt:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const zugang = await partner.getCustomerCredentials(customerId);
|
|
247
|
+
|
|
248
|
+
console.log(zugang); // [apiKey «verborgen»] — kein Klartext
|
|
249
|
+
JSON.stringify(zugang); // ebenso
|
|
250
|
+
`${zugang.apiKey}`; // ebenso
|
|
251
|
+
|
|
252
|
+
speichereVerschluesselt(zugang.apiKey.reveal()); // der einzige Weg heraus
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Nur verschlüsselt speichern, nie protokollieren, nie in eine Mail oder einen
|
|
256
|
+
Fehlerbericht. Jeder Abruf wird mitgeschrieben und ist für den Betrieb
|
|
257
|
+
sichtbar.
|
|
258
|
+
|
|
259
|
+
### Eingehende Webhooks prüfen
|
|
260
|
+
|
|
261
|
+
Das ist die Stelle, an der Integrationen am häufigsten scheitern — deshalb
|
|
262
|
+
liegt sie fertig im Paket. Vier Dinge müssen stimmen: der **rohe** Rumpf, das
|
|
263
|
+
Zeitfenster gegen Wiedereinspielung, ein zeitkonstanter Vergleich, und jede
|
|
264
|
+
Ausnahme als Ablehnung.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import express from 'express';
|
|
268
|
+
import { parseWebhookEvent } from '@kreiseck/kasseneck-api/partner';
|
|
269
|
+
|
|
270
|
+
const app = express();
|
|
271
|
+
|
|
272
|
+
// express.raw VOR jedem JSON-Parser: signiert sind die Bytes, die ankommen.
|
|
273
|
+
app.post('/kasseneck-webhook', express.raw({ type: '*/*' }), async (req, res) => {
|
|
274
|
+
const ergebnis = await parseWebhookEvent({
|
|
275
|
+
secret: process.env.KASSENECK_WEBHOOK_SECRET!,
|
|
276
|
+
signatureHeader: req.header('X-Kasseneck-Signature'),
|
|
277
|
+
body: req.body, // Buffer — nicht req.body nach JSON.parse
|
|
278
|
+
});
|
|
279
|
+
if (!ergebnis.ok) return res.status(400).send(ergebnis.reason);
|
|
280
|
+
|
|
281
|
+
// Innerhalb von 10 s antworten, Arbeit danach. Zustellungen können sich
|
|
282
|
+
// wiederholen: auf event.id entdoppeln.
|
|
283
|
+
res.sendStatus(200);
|
|
284
|
+
await verarbeite(ergebnis.event);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
170
288
|
## Unterpfade
|
|
171
289
|
|
|
172
290
|
| Unterpfad | Inhalt |
|
|
@@ -177,6 +295,7 @@ den Storno-Dialog); die Wahrheit hat der Server.
|
|
|
177
295
|
| `…/payments` | Stripe-Zahllinks und Hobex-Cloud (beides HTTP-Endpunkte des Backends). |
|
|
178
296
|
| `…/register` | Anmeldung der Browser-Kasse: Gerät koppeln und entkoppeln, Benutzer auflisten, per PIN anmelden, Sitzung erneuern und beenden. |
|
|
179
297
|
| `…/kasse` | Kachel-Kasse: Kassen-Einstellungen (betriebsweit / je Gerät), Artikelgruppen und Artikel für Kacheln, Rabattverteilung je Steuersatz, Reichweiten der Kassen-Rechte |
|
|
298
|
+
| `…/partner` | Partner-API: Betriebe anlegen, FinanzOnline-Link, Signatur, Kassen, Zugangsdaten, Webhooks samt Signaturprüfung. **Gehört auf einen Server.** |
|
|
180
299
|
| `…/react` | Dünner React-Adapter, der ein Beleg-Layout zeichnet. Braucht React. |
|
|
181
300
|
| `…/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. |
|
|
182
301
|
|
|
@@ -199,12 +318,33 @@ PDF-Erzeugung.
|
|
|
199
318
|
npm test # Testsuite in drei Zeitzonen (Wien, UTC, Kiritimati)
|
|
200
319
|
npm run build # ESM- und CJS-Bau nach dist/, inkl. Prüfung der exports
|
|
201
320
|
npm run check:consumer # baut den Tarball und übersetzt zwei Verbraucher (CJS/ESM)
|
|
321
|
+
npm run check:erreichbar # fragt die öffentliche Adresse: antwortet dort zu jedem Aufruf eine Function?
|
|
202
322
|
```
|
|
203
323
|
|
|
204
324
|
Die drei Zeitzonen sind kein Übereifer: Zeitfehler sind auf einer Wiener
|
|
205
325
|
Maschine zufällig richtig. Belegzeiten werden konsequent als **Wiener
|
|
206
326
|
Wanduhrzeit** gedeutet (`parseServerTimeStamp`), nie über `new Date(text)`.
|
|
207
327
|
|
|
328
|
+
### `check:erreichbar` — spricht als einzige mit `api.kasseneck.at`
|
|
329
|
+
|
|
330
|
+
Testsuite und `check:consumer` laufen gegen Attrappen bzw. gegen den Tarball;
|
|
331
|
+
keine von beiden setzt je einen Aufruf ab. Fehlt einem Aufruf die
|
|
332
|
+
Hosting-Weiterleitung, liefert die veröffentlichte Adresse die
|
|
333
|
+
HTML-Auffangseite statt der Function — und das sieht keine Attrappe.
|
|
334
|
+
|
|
335
|
+
Die Prüfung braucht **keine Zugangsdaten**. Ein Aufruf ohne Anmeldung
|
|
336
|
+
antwortet, wenn dort eine Function steht, mit
|
|
337
|
+
`{"status":"error","message":"Ungültiger Request: Authorization key erwartet."}`.
|
|
338
|
+
Genau das ist der Beweis: Der Aufruf wurde angenommen und die Anmeldung
|
|
339
|
+
geprüft. Eine HTML-Seite oder ein 404 ist der Beweis, dass dort keine Function
|
|
340
|
+
steht. Deshalb prüft das Skript auf ein `status`-Feld und nicht auf Erfolg.
|
|
341
|
+
|
|
342
|
+
Bewusst außerhalb von `npm test`: Sie braucht Netz. Ist keines da, sagt sie es
|
|
343
|
+
und endet mit 0. Aufrufe, die unter `/v1` absichtlich keine Weiterleitung
|
|
344
|
+
haben — der Kassen-Weg über `kasse.kasseneck.at/api`, die Aufrufe mit
|
|
345
|
+
Firebase-ID-Token — stehen mit Grund in `scripts/erreichbarkeit-ausnahmen.json`.
|
|
346
|
+
Wird eine Ausnahme erreichbar, schlägt die Prüfung an: Sonst sänke die Zahl nie.
|
|
347
|
+
|
|
208
348
|
## Vertragsdateien für die Zwillinge
|
|
209
349
|
|
|
210
350
|
Dieses Paket ist die Quelle für das Dart-Paket `kasseneck_api` und den
|
|
@@ -214,7 +354,7 @@ im Tarball mit und sagen in Maschinenform, worauf sich alle drei geeinigt haben:
|
|
|
214
354
|
| Datei | Inhalt |
|
|
215
355
|
|---|---|
|
|
216
356
|
| `kasse-settings-standard.json` | Feldnamen und Standardwerte der Kassen-Einstellungen |
|
|
217
|
-
| `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen |
|
|
357
|
+
| `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen, Partner-Listen |
|
|
218
358
|
|
|
219
359
|
Beide werden erzeugt (`npm run fixtures:kasse`, `npm run fixtures:oberflaeche`)
|
|
220
360
|
und nie von Hand geändert; die CI prüft nach jedem Lauf, dass sie zum Code passen.
|
|
@@ -222,6 +362,22 @@ und nie von Hand geändert; die CI prüft nach jedem Lauf, dass sie zum Code pas
|
|
|
222
362
|
`oberflaeche.json` trägt die Paketversion. **Nach jedem `npm version` müssen deshalb beide
|
|
223
363
|
Dateien neu erzeugt und mitcommittet werden**, sonst wird die CI rot.
|
|
224
364
|
|
|
365
|
+
### Und die Gegenrichtung
|
|
366
|
+
|
|
367
|
+
Der Vertrag in `fixtures/` wird **drüben** geprüft: das Dart-Repo zieht ihn und
|
|
368
|
+
hält seine Listen dagegen. Eine Lücke fiele hier deshalb erst im nächsten
|
|
369
|
+
Zwillingslauf im anderen Repo auf — an einem anderen Tag. Dagegen stehen zwei
|
|
370
|
+
von Hand gepflegte Abzüge der Dart-Seite unter `test/fixtures/`, jeder mit
|
|
371
|
+
`_quelle`:
|
|
372
|
+
|
|
373
|
+
| Datei | prüft |
|
|
374
|
+
|---|---|
|
|
375
|
+
| `dart-enums.json` | Belegtyp, Steuersatz, Zahlungsart, Kartenanbieter, Gutschein, Stripe-Modus |
|
|
376
|
+
| `dart-partner.json` | Umgebungen, Fehlercodes (API und Portal), Webhook-Ereignisse, Felder des Webhook-Umschlags samt der Marke `test`, Betriebsfelder, Wiederholungsplan |
|
|
377
|
+
|
|
378
|
+
Sie machen `npm test` rot, sobald ein Wert nur in einer der beiden Sprachen
|
|
379
|
+
ankommt.
|
|
380
|
+
|
|
225
381
|
## Lizenz
|
|
226
382
|
|
|
227
383
|
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", "listRegisterUsersForDevice", "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", "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,33 +2,53 @@
|
|
|
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',
|
|
36
|
+
'listMyTipRecipients',
|
|
37
|
+
'listPartnerCustomers',
|
|
38
|
+
'listPartnerWebhookDeliveries',
|
|
39
|
+
'listPartnerWebhooks',
|
|
25
40
|
'listRegisterUsersForDevice',
|
|
26
41
|
'pairRegisterDevice',
|
|
27
42
|
'registerPinLogin',
|
|
28
43
|
'registerUserLogin',
|
|
29
44
|
'renewRegisterSession',
|
|
45
|
+
'requestCustomerSignature',
|
|
46
|
+
'sendPartnerCustomerFonLink',
|
|
47
|
+
'sendPartnerWebhookTest',
|
|
30
48
|
'setMyKasseSettings',
|
|
31
49
|
'setMyRegisterDeviceSettings',
|
|
32
50
|
'stripeCaptureIntent',
|
|
33
51
|
'unpairRegisterDevice',
|
|
52
|
+
'rotatePartnerWebhookSecret',
|
|
53
|
+
'updatePartnerWebhook',
|
|
34
54
|
];
|
|
@@ -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";
|
|
@@ -76,7 +94,19 @@ export declare class KasseneckApiError extends Error {
|
|
|
76
94
|
readonly functionName: string;
|
|
77
95
|
/** Meldung des Backends, unveraendert (`message` aus der Huelle). */
|
|
78
96
|
readonly serverMessage: string;
|
|
79
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Maschinenlesbarer Fehlercode aus `data.code`, sofern die Antwort einen
|
|
99
|
+
* fuehrt (`vertrag_offen`, `rate_limited`, …). Die aelteren Endpunkte des
|
|
100
|
+
* Backends antworten ohne Code — dann `undefined`.
|
|
101
|
+
*/
|
|
102
|
+
readonly code: string | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Die uebrige Fehler-Nutzlast, gesiebt (siehe [fehlerDetails]): `schritt`,
|
|
105
|
+
* `rc`, `retryAfterSec`, `errors[]` und was der jeweilige Endpunkt sonst
|
|
106
|
+
* beilegt. Immer ein Objekt, notfalls ein leeres.
|
|
107
|
+
*/
|
|
108
|
+
readonly details: Record<string, unknown>;
|
|
109
|
+
constructor(functionName: string, serverMessage: string, details?: Record<string, unknown>);
|
|
80
110
|
}
|
|
81
111
|
/** Warum die Antwort keine verwertbare Huelle war. */
|
|
82
112
|
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';
|
|
@@ -101,10 +180,28 @@ class KasseneckApiError extends Error {
|
|
|
101
180
|
functionName;
|
|
102
181
|
/** Meldung des Backends, unveraendert (`message` aus der Huelle). */
|
|
103
182
|
serverMessage;
|
|
104
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Maschinenlesbarer Fehlercode aus `data.code`, sofern die Antwort einen
|
|
185
|
+
* fuehrt (`vertrag_offen`, `rate_limited`, …). Die aelteren Endpunkte des
|
|
186
|
+
* Backends antworten ohne Code — dann `undefined`.
|
|
187
|
+
*/
|
|
188
|
+
code;
|
|
189
|
+
/**
|
|
190
|
+
* Die uebrige Fehler-Nutzlast, gesiebt (siehe [fehlerDetails]): `schritt`,
|
|
191
|
+
* `rc`, `retryAfterSec`, `errors[]` und was der jeweilige Endpunkt sonst
|
|
192
|
+
* beilegt. Immer ein Objekt, notfalls ein leeres.
|
|
193
|
+
*/
|
|
194
|
+
details;
|
|
195
|
+
constructor(functionName, serverMessage, details = {}) {
|
|
105
196
|
super(`${functionName} fehlgeschlagen: ${serverMessage}`);
|
|
106
197
|
this.functionName = functionName;
|
|
107
198
|
this.serverMessage = serverMessage;
|
|
199
|
+
this.details = details;
|
|
200
|
+
// Der Code steht in `details` und zusaetzlich als eigenes Feld: das eine
|
|
201
|
+
// ist die Nutzlast, das andere die Frage, die ein Aufrufer wirklich
|
|
202
|
+
// stellt. Nur Bezeichner gelten — ein Freitext waere kein Code.
|
|
203
|
+
const code = details['code'];
|
|
204
|
+
this.code = typeof code === 'string' && BEZEICHNER.test(code) ? code : undefined;
|
|
108
205
|
}
|
|
109
206
|
}
|
|
110
207
|
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);
|
|
184
|
+
throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
|
|
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);
|
|
230
|
+
throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
|
|
231
231
|
}
|
|
232
232
|
/** `%PDF` am Anfang — die Kennung jeder PDF-Datei. */
|
|
233
233
|
function istPdf(bytes) {
|
|
@@ -240,9 +240,9 @@ function alsHuelle(wert) {
|
|
|
240
240
|
}
|
|
241
241
|
return wert;
|
|
242
242
|
}
|
|
243
|
-
function fachfehler(functionName, message) {
|
|
243
|
+
function fachfehler(functionName, message, daten, geheimnisse) {
|
|
244
244
|
const meldung = typeof message === 'string' && message.trim() ? message : 'Unbekannter Fehler';
|
|
245
|
-
return new errors_js_1.KasseneckApiError(functionName, meldung);
|
|
245
|
+
return new errors_js_1.KasseneckApiError(functionName, meldung, (0, errors_js_1.fehlerDetails)(daten, geheimnisse));
|
|
246
246
|
}
|
|
247
247
|
/**
|
|
248
248
|
* Nutzlast aus Auth- und Aufruferparametern. Auth-Parameter bilden die
|
|
@@ -9,3 +9,4 @@ export { getKasseSettings, setMyKasseSettings, setMyRegisterDeviceSettings } fro
|
|
|
9
9
|
export { verteileRabatt } from '../receipt/discount.js';
|
|
10
10
|
export { cancelScopeOf, receiptsScopeOf, type RegisterScope, type RegisterUserPerms } from '../register/pairing.js';
|
|
11
11
|
export { type NetzDrucker, type DruckJob, type DruckJobStatus, type CreatePrintJobOptions, listMyPrinters, createPrintJob, getPrintJob } from './drucker.js';
|
|
12
|
+
export { listMyTipRecipients } from './trinkgeld.js';
|