@kreiseck/kasseneck-api 0.6.46 → 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 +19 -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/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 +31 -1
- package/dist/esm/client/errors.js +97 -1
- 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/oberflaeche.json +131 -3
- package/package.json +13 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhooks: Endpunkte verwalten und eingehende Zustellungen auswerten.
|
|
3
|
+
*
|
|
4
|
+
* Die Signaturpruefung liegt daneben in `webhook-signatur.ts` — sie braucht
|
|
5
|
+
* weder Transport noch Schluessel und laeuft in jedem Empfaenger, auch in
|
|
6
|
+
* einem, der sonst nichts von diesem Paket benutzt.
|
|
7
|
+
*/
|
|
8
|
+
import type { InternerTransport } from '../client/aufrufe.js';
|
|
9
|
+
import { type VerifyWebhookOptions, type WebhookVerifyReason } from './webhook-signatur.js';
|
|
10
|
+
/**
|
|
11
|
+
* Alle Ereignisse, die ein Webhook abonnieren **und proben** kann. Ein
|
|
12
|
+
* Endpunkt bekommt ausschliesslich die, die in seiner `events`-Liste stehen.
|
|
13
|
+
*
|
|
14
|
+
* Kasseneck fuehrt daneben interne Ereignisse (etwa den Abschluss eines
|
|
15
|
+
* Auftragsverarbeitungsvertrags). Sie stehen hier bewusst nicht: sie lassen
|
|
16
|
+
* sich weder abonnieren noch mit [sendPartnerWebhookTest] ausloesen, und ein
|
|
17
|
+
* Name in dieser Liste, den niemand bestellen kann, waere ein Versprechen ohne
|
|
18
|
+
* Deckung.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PARTNER_WEBHOOK_EVENTS: readonly ["customer.created", "customer.updated", "customer.status_changed", "customer.fon_verified", "customer.live_enabled", "signature.requested", "signature.ready", "signature.failed", "cashregister.created", "cashregister.live", "cashregister.failed", "app.version.accepted", "app.version.rejected", "webhook.test"];
|
|
21
|
+
export type PartnerWebhookEventType = typeof PARTNER_WEBHOOK_EVENTS[number];
|
|
22
|
+
export declare function istPartnerWebhookEvent(wert: unknown): wert is PartnerWebhookEventType;
|
|
23
|
+
/**
|
|
24
|
+
* Die Felder des Umschlags, so wie er auf der Leitung liegt.
|
|
25
|
+
*
|
|
26
|
+
* Als Liste, damit der Zwilling sie nachhaelt: `test` kam spaeter dazu, und
|
|
27
|
+
* genau ein solches Feld verschwindet sonst auf einer Seite, ohne dass etwas
|
|
28
|
+
* rot wird.
|
|
29
|
+
*/
|
|
30
|
+
export declare const WEBHOOK_UMSCHLAG_FELDER: readonly ["id", "type", "createdAt", "partnerId", "test", "data"];
|
|
31
|
+
/**
|
|
32
|
+
* Die Huelle jeder Zustellung. `type` bleibt bewusst offen fuer unbekannte
|
|
33
|
+
* Werte (`(string & {})`): ein spaeter ergaenztes Ereignis soll einen
|
|
34
|
+
* Empfaenger nicht zum Absturz bringen, sondern in seinem `default`-Zweig
|
|
35
|
+
* landen.
|
|
36
|
+
*/
|
|
37
|
+
export interface PartnerWebhookEvent<T = Record<string, unknown>> {
|
|
38
|
+
/** `evt_…` — die Kennung, auf die **entdoppelt** wird. */
|
|
39
|
+
id: string;
|
|
40
|
+
type: PartnerWebhookEventType | (string & {});
|
|
41
|
+
createdAt: number;
|
|
42
|
+
partnerId: string;
|
|
43
|
+
/**
|
|
44
|
+
* **Probe oder Ernstfall.** Eine mit [sendPartnerWebhookTest] ausgeloeste
|
|
45
|
+
* Zustellung traegt `test: true` im Umschlag; ein echtes Ereignis fuehrt das
|
|
46
|
+
* Feld gar nicht, hier steht dann `false`.
|
|
47
|
+
*
|
|
48
|
+
* Diese Zeile gehoert an den Anfang jedes Handlers:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* if (ereignis.test) return;
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* Ohne sie haelt jemand eine Probe fuer echt und schreibt seinem Kunden, die
|
|
55
|
+
* Kasse sei fertig. Eine Probe traegt eine erkennbar erfundene Nutzlast — nur
|
|
56
|
+
* sieht man das erst, wenn man hinsieht.
|
|
57
|
+
*/
|
|
58
|
+
test: boolean;
|
|
59
|
+
data: T;
|
|
60
|
+
}
|
|
61
|
+
export type WebhookEventResult = {
|
|
62
|
+
ok: true;
|
|
63
|
+
event: PartnerWebhookEvent;
|
|
64
|
+
timestampSec: number;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: WebhookVerifyReason | 'body-not-json' | 'body-not-event';
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Prueft die Signatur **und** liest das Ereignis — in dieser Reihenfolge. Wer
|
|
71
|
+
* zuerst parst und dann prueft, hat den fremden Rumpf schon durch seinen Code
|
|
72
|
+
* laufen lassen.
|
|
73
|
+
*
|
|
74
|
+
* **Entdoppeln nicht vergessen:** dieselbe Zustellung kann mehrfach ankommen
|
|
75
|
+
* (Wiederholung nach einer Antwort, die unterwegs verloren ging). Massgeblich
|
|
76
|
+
* ist `event.id`, nicht der Kopf `X-Kasseneck-Delivery` — der ist bei
|
|
77
|
+
* Wiederholungen derselbe, aber er beantwortet die Frage "habe ich dieses
|
|
78
|
+
* Ereignis schon verarbeitet?" nur fuer genau diesen Endpunkt.
|
|
79
|
+
*
|
|
80
|
+
* Antworte innerhalb von 10 Sekunden mit 2xx und erledige die Arbeit danach.
|
|
81
|
+
* Eine ausbleibende Antwort wird bis zu fuenfmal wiederholt (1 min, 5 min,
|
|
82
|
+
* 30 min, 2 h, 12 h) und gilt dann als fehlgeschlagen.
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseWebhookEvent(optionen: VerifyWebhookOptions): Promise<WebhookEventResult>;
|
|
85
|
+
export interface PartnerWebhook {
|
|
86
|
+
webhookId: string;
|
|
87
|
+
url: string;
|
|
88
|
+
events: string[];
|
|
89
|
+
active: boolean;
|
|
90
|
+
description: string | null;
|
|
91
|
+
createdAt: number | null;
|
|
92
|
+
lastDelivery: number | null;
|
|
93
|
+
/** Fehlversuche in Folge — steigt der Wert, stimmt beim Empfaenger etwas nicht. */
|
|
94
|
+
consecutiveFailures: number;
|
|
95
|
+
}
|
|
96
|
+
export interface CreateWebhookOptions {
|
|
97
|
+
/** Vollstaendige `https://`-Adresse. */
|
|
98
|
+
url: string;
|
|
99
|
+
/** Mindestens eines; unbekannte Namen lehnt das Backend ab. */
|
|
100
|
+
events: (PartnerWebhookEventType | (string & {}))[];
|
|
101
|
+
/** Hoechstens 120 Zeichen. */
|
|
102
|
+
description?: string;
|
|
103
|
+
active?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface CreateWebhookResult {
|
|
106
|
+
webhook: PartnerWebhook;
|
|
107
|
+
/**
|
|
108
|
+
* Das Secret fuer die Signaturpruefung. **Es kommt genau einmal** — beim
|
|
109
|
+
* Anlegen. Danach gibt das Backend es nie wieder aus; wer es verliert, legt
|
|
110
|
+
* einen neuen Endpunkt an.
|
|
111
|
+
*
|
|
112
|
+
* Bewusst ein `string` und kein [KasseneckSecret]: es gehoert dem Partner
|
|
113
|
+
* selbst und nicht einem fremden Betrieb, und es muss unveraendert in die
|
|
114
|
+
* eigene Konfiguration wandern. Verschluesselt speichern gilt trotzdem.
|
|
115
|
+
*/
|
|
116
|
+
secret: string;
|
|
117
|
+
}
|
|
118
|
+
export interface WebhookPatch {
|
|
119
|
+
url?: string;
|
|
120
|
+
events?: (PartnerWebhookEventType | (string & {}))[];
|
|
121
|
+
description?: string;
|
|
122
|
+
active?: boolean;
|
|
123
|
+
}
|
|
124
|
+
export interface WebhookListe {
|
|
125
|
+
webhooks: PartnerWebhook[];
|
|
126
|
+
/** Der Katalog: Ereignisname und deutscher Text, so wie das Panel ihn zeigt. */
|
|
127
|
+
events: {
|
|
128
|
+
key: string;
|
|
129
|
+
text: string;
|
|
130
|
+
}[];
|
|
131
|
+
}
|
|
132
|
+
export interface WebhookZustellung {
|
|
133
|
+
deliveryId: string;
|
|
134
|
+
webhookId: string;
|
|
135
|
+
event: string;
|
|
136
|
+
eventId: string;
|
|
137
|
+
/** `offen`, `zugestellt` oder `fehlgeschlagen`. */
|
|
138
|
+
status: string;
|
|
139
|
+
attempts: number;
|
|
140
|
+
letzterVersuchAt: number | null;
|
|
141
|
+
naechsterVersuchAt: number | null;
|
|
142
|
+
statusCode: number | null;
|
|
143
|
+
/** Auszug der Antwort des Empfaengers, hoechstens 500 Zeichen. */
|
|
144
|
+
response: string | null;
|
|
145
|
+
createdAt: number | null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Legt einen Webhook-Endpunkt an. Hoechstens [WEBHOOK_LIMIT] je Partner
|
|
149
|
+
* (`webhook_limit`).
|
|
150
|
+
*
|
|
151
|
+
* **Das Secret in der Antwort ist der einzige Weg dazu.** Es sofort dorthin
|
|
152
|
+
* schreiben, wo der Empfaenger es liest — nicht in ein Protokoll.
|
|
153
|
+
*/
|
|
154
|
+
export declare function createPartnerWebhook(rufen: InternerTransport, optionen: CreateWebhookOptions): Promise<CreateWebhookResult>;
|
|
155
|
+
/**
|
|
156
|
+
* Ein neues Secret fuer denselben Endpunkt.
|
|
157
|
+
*
|
|
158
|
+
* Der Webhook behaelt seine `webhookId` und seine Ereignisse — nur das
|
|
159
|
+
* Geheimnis wechselt. Wer den Endpunkt stattdessen loescht und neu anlegt,
|
|
160
|
+
* verliert seine Zustellungshistorie und muss die Ereignisse neu ankreuzen.
|
|
161
|
+
*
|
|
162
|
+
* Das alte Secret gilt ab der Antwort NICHT mehr: die naechste Zustellung ist
|
|
163
|
+
* schon mit dem neuen signiert. Erst speichern, dann weiterarbeiten.
|
|
164
|
+
*/
|
|
165
|
+
export declare function rotatePartnerWebhookSecret(rufen: InternerTransport, webhookId: string): Promise<CreateWebhookResult>;
|
|
166
|
+
/** Die Webhook-Endpunkte dieses Partners samt Ereignis-Katalog. */
|
|
167
|
+
export declare function listPartnerWebhooks(rufen: InternerTransport): Promise<WebhookListe>;
|
|
168
|
+
/**
|
|
169
|
+
* Aendert einen Endpunkt. Nur die genannten Felder — ein leeres `patch` lehnt
|
|
170
|
+
* das Backend ab (`validation`, Feld `patch`), statt stillschweigend nichts zu
|
|
171
|
+
* tun.
|
|
172
|
+
*/
|
|
173
|
+
export declare function updatePartnerWebhook(rufen: InternerTransport, webhookId: string, patch: WebhookPatch): Promise<PartnerWebhook>;
|
|
174
|
+
/** Loescht einen Endpunkt. Danach kommt dort nichts mehr an. */
|
|
175
|
+
export declare function deletePartnerWebhook(rufen: InternerTransport, webhookId: string): Promise<string>;
|
|
176
|
+
export interface WebhookTestResult {
|
|
177
|
+
eventId: string;
|
|
178
|
+
/** Welches Ereignis geprobt wurde — ohne Angabe `webhook.test`. */
|
|
179
|
+
ereignis: string;
|
|
180
|
+
deliveries: unknown[];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Schickt eine Probe an genau diesen Endpunkt.
|
|
184
|
+
*
|
|
185
|
+
* Ohne `event` kommt `webhook.test` — der Nachweis, dass die Leitung steht und
|
|
186
|
+
* die eigene Signaturpruefung gegen echte Bytes laeuft. **Mit `event` kommt
|
|
187
|
+
* genau das Ereignis, das der Empfaenger behandeln soll**, mit einer
|
|
188
|
+
* glaubwuerdigen Nutzlast: wer auf `signature.ready` hin seinen Kunden
|
|
189
|
+
* benachrichtigt, probt das einmal, statt auf eine echte Karte zu warten. Eine
|
|
190
|
+
* Leitungsprobe beweist nichts ueber die Behandlung des Ernstfalls.
|
|
191
|
+
*
|
|
192
|
+
* Der Endpunkt muss das Ereignis abonnieren (`event_not_subscribed`) und aktiv
|
|
193
|
+
* sein (`webhook_inactive`); ein unbekannter Name ist ein `validation`-Fehler
|
|
194
|
+
* auf dem Feld `event`.
|
|
195
|
+
*
|
|
196
|
+
* **Jede Probe traegt `test: true` im Umschlag** ([PartnerWebhookEvent.test]).
|
|
197
|
+
*
|
|
198
|
+
* Der Backend-Endpunkt heisst `sendPartnerWebhookTest`; dieser Client behaelt
|
|
199
|
+
* den Namen bei, damit ein Leser der Doku und ein Leser des Codes dasselbe
|
|
200
|
+
* suchen.
|
|
201
|
+
*/
|
|
202
|
+
export declare function sendPartnerWebhookTest(rufen: InternerTransport, webhookId: string, event?: PartnerWebhookEventType | (string & {})): Promise<WebhookTestResult>;
|
|
203
|
+
/**
|
|
204
|
+
* Die letzten Zustellversuche — mit `webhookId` fuer einen Endpunkt, ohne ihn
|
|
205
|
+
* fuer alle. Die Stelle, an der sich „mein Server bekommt nichts" klaeren
|
|
206
|
+
* laesst, ohne bei Kasseneck nachzufragen.
|
|
207
|
+
*/
|
|
208
|
+
export declare function listPartnerWebhookDeliveries(rufen: InternerTransport, optionen?: {
|
|
209
|
+
webhookId?: string;
|
|
210
|
+
limit?: number;
|
|
211
|
+
}): Promise<WebhookZustellung[]>;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhooks: Endpunkte verwalten und eingehende Zustellungen auswerten.
|
|
4
|
+
*
|
|
5
|
+
* Die Signaturpruefung liegt daneben in `webhook-signatur.ts` — sie braucht
|
|
6
|
+
* weder Transport noch Schluessel und laeuft in jedem Empfaenger, auch in
|
|
7
|
+
* einem, der sonst nichts von diesem Paket benutzt.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.WEBHOOK_UMSCHLAG_FELDER = exports.PARTNER_WEBHOOK_EVENTS = void 0;
|
|
11
|
+
exports.istPartnerWebhookEvent = istPartnerWebhookEvent;
|
|
12
|
+
exports.parseWebhookEvent = parseWebhookEvent;
|
|
13
|
+
exports.createPartnerWebhook = createPartnerWebhook;
|
|
14
|
+
exports.rotatePartnerWebhookSecret = rotatePartnerWebhookSecret;
|
|
15
|
+
exports.listPartnerWebhooks = listPartnerWebhooks;
|
|
16
|
+
exports.updatePartnerWebhook = updatePartnerWebhook;
|
|
17
|
+
exports.deletePartnerWebhook = deletePartnerWebhook;
|
|
18
|
+
exports.sendPartnerWebhookTest = sendPartnerWebhookTest;
|
|
19
|
+
exports.listPartnerWebhookDeliveries = listPartnerWebhookDeliveries;
|
|
20
|
+
const errors_js_1 = require("../client/errors.js");
|
|
21
|
+
const webhook_signatur_js_1 = require("./webhook-signatur.js");
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Ereignisse
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Alle Ereignisse, die ein Webhook abonnieren **und proben** kann. Ein
|
|
27
|
+
* Endpunkt bekommt ausschliesslich die, die in seiner `events`-Liste stehen.
|
|
28
|
+
*
|
|
29
|
+
* Kasseneck fuehrt daneben interne Ereignisse (etwa den Abschluss eines
|
|
30
|
+
* Auftragsverarbeitungsvertrags). Sie stehen hier bewusst nicht: sie lassen
|
|
31
|
+
* sich weder abonnieren noch mit [sendPartnerWebhookTest] ausloesen, und ein
|
|
32
|
+
* Name in dieser Liste, den niemand bestellen kann, waere ein Versprechen ohne
|
|
33
|
+
* Deckung.
|
|
34
|
+
*/
|
|
35
|
+
exports.PARTNER_WEBHOOK_EVENTS = [
|
|
36
|
+
'customer.created',
|
|
37
|
+
'customer.updated',
|
|
38
|
+
'customer.status_changed',
|
|
39
|
+
'customer.fon_verified',
|
|
40
|
+
'customer.live_enabled',
|
|
41
|
+
'signature.requested',
|
|
42
|
+
'signature.ready',
|
|
43
|
+
'signature.failed',
|
|
44
|
+
'cashregister.created',
|
|
45
|
+
'cashregister.live',
|
|
46
|
+
'cashregister.failed',
|
|
47
|
+
'app.version.accepted',
|
|
48
|
+
'app.version.rejected',
|
|
49
|
+
'webhook.test',
|
|
50
|
+
];
|
|
51
|
+
function istPartnerWebhookEvent(wert) {
|
|
52
|
+
return typeof wert === 'string' && exports.PARTNER_WEBHOOK_EVENTS.includes(wert);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Die Felder des Umschlags, so wie er auf der Leitung liegt.
|
|
56
|
+
*
|
|
57
|
+
* Als Liste, damit der Zwilling sie nachhaelt: `test` kam spaeter dazu, und
|
|
58
|
+
* genau ein solches Feld verschwindet sonst auf einer Seite, ohne dass etwas
|
|
59
|
+
* rot wird.
|
|
60
|
+
*/
|
|
61
|
+
exports.WEBHOOK_UMSCHLAG_FELDER = ['id', 'type', 'createdAt', 'partnerId', 'test', 'data'];
|
|
62
|
+
/**
|
|
63
|
+
* Prueft die Signatur **und** liest das Ereignis — in dieser Reihenfolge. Wer
|
|
64
|
+
* zuerst parst und dann prueft, hat den fremden Rumpf schon durch seinen Code
|
|
65
|
+
* laufen lassen.
|
|
66
|
+
*
|
|
67
|
+
* **Entdoppeln nicht vergessen:** dieselbe Zustellung kann mehrfach ankommen
|
|
68
|
+
* (Wiederholung nach einer Antwort, die unterwegs verloren ging). Massgeblich
|
|
69
|
+
* ist `event.id`, nicht der Kopf `X-Kasseneck-Delivery` — der ist bei
|
|
70
|
+
* Wiederholungen derselbe, aber er beantwortet die Frage "habe ich dieses
|
|
71
|
+
* Ereignis schon verarbeitet?" nur fuer genau diesen Endpunkt.
|
|
72
|
+
*
|
|
73
|
+
* Antworte innerhalb von 10 Sekunden mit 2xx und erledige die Arbeit danach.
|
|
74
|
+
* Eine ausbleibende Antwort wird bis zu fuenfmal wiederholt (1 min, 5 min,
|
|
75
|
+
* 30 min, 2 h, 12 h) und gilt dann als fehlgeschlagen.
|
|
76
|
+
*/
|
|
77
|
+
async function parseWebhookEvent(optionen) {
|
|
78
|
+
const geprueft = await (0, webhook_signatur_js_1.verifyWebhookSignature)(optionen);
|
|
79
|
+
if (!geprueft.ok)
|
|
80
|
+
return { ok: false, reason: geprueft.reason };
|
|
81
|
+
const text = typeof optionen.body === 'string' ? optionen.body : new TextDecoder('utf-8').decode(optionen.body);
|
|
82
|
+
let roh;
|
|
83
|
+
try {
|
|
84
|
+
roh = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { ok: false, reason: 'body-not-json' };
|
|
88
|
+
}
|
|
89
|
+
if (roh === null || typeof roh !== 'object' || Array.isArray(roh))
|
|
90
|
+
return { ok: false, reason: 'body-not-event' };
|
|
91
|
+
const e = roh;
|
|
92
|
+
if (typeof e['id'] !== 'string' || typeof e['type'] !== 'string')
|
|
93
|
+
return { ok: false, reason: 'body-not-event' };
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
timestampSec: geprueft.timestampSec,
|
|
97
|
+
event: {
|
|
98
|
+
id: e['id'],
|
|
99
|
+
type: e['type'],
|
|
100
|
+
createdAt: typeof e['createdAt'] === 'number' ? e['createdAt'] : 0,
|
|
101
|
+
partnerId: typeof e['partnerId'] === 'string' ? e['partnerId'] : '',
|
|
102
|
+
// Nur ein ausdrueckliches `true` ist eine Probe. Alles andere — auch ein
|
|
103
|
+
// fehlendes Feld — ist der Ernstfall; im Zweifel lieber einmal zu viel
|
|
104
|
+
// gearbeitet als eine echte Kasse fuer eine Probe gehalten.
|
|
105
|
+
test: e['test'] === true,
|
|
106
|
+
data: e['data'] !== null && typeof e['data'] === 'object' && !Array.isArray(e['data'])
|
|
107
|
+
? e['data']
|
|
108
|
+
: {},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function objekt(wert) {
|
|
113
|
+
return wert !== null && typeof wert === 'object' && !Array.isArray(wert) ? wert : {};
|
|
114
|
+
}
|
|
115
|
+
function webhook(eintrag) {
|
|
116
|
+
const w = objekt(eintrag);
|
|
117
|
+
return {
|
|
118
|
+
webhookId: typeof w['webhookId'] === 'string' ? w['webhookId'] : '',
|
|
119
|
+
url: typeof w['url'] === 'string' ? w['url'] : '',
|
|
120
|
+
events: Array.isArray(w['events']) ? w['events'].filter((e) => typeof e === 'string') : [],
|
|
121
|
+
active: w['active'] !== false,
|
|
122
|
+
description: typeof w['description'] === 'string' ? w['description'] : null,
|
|
123
|
+
createdAt: typeof w['createdAt'] === 'number' ? w['createdAt'] : null,
|
|
124
|
+
lastDelivery: typeof w['lastDelivery'] === 'number' ? w['lastDelivery'] : null,
|
|
125
|
+
consecutiveFailures: typeof w['consecutiveFailures'] === 'number' ? w['consecutiveFailures'] : 0,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Legt einen Webhook-Endpunkt an. Hoechstens [WEBHOOK_LIMIT] je Partner
|
|
130
|
+
* (`webhook_limit`).
|
|
131
|
+
*
|
|
132
|
+
* **Das Secret in der Antwort ist der einzige Weg dazu.** Es sofort dorthin
|
|
133
|
+
* schreiben, wo der Empfaenger es liest — nicht in ein Protokoll.
|
|
134
|
+
*/
|
|
135
|
+
async function createPartnerWebhook(rufen, optionen) {
|
|
136
|
+
const url = typeof optionen?.url === 'string' ? optionen.url.trim() : '';
|
|
137
|
+
if (!url)
|
|
138
|
+
throw new errors_js_1.KasseneckValidationError('createPartnerWebhook', 'url fehlt', 'request');
|
|
139
|
+
if (!Array.isArray(optionen.events) || optionen.events.length === 0) {
|
|
140
|
+
throw new errors_js_1.KasseneckValidationError('createPartnerWebhook', 'events ist leer — ein Endpunkt ohne Ereignis bekaeme nie etwas', 'request');
|
|
141
|
+
}
|
|
142
|
+
const daten = objekt(await rufen('createPartnerWebhook', {
|
|
143
|
+
url,
|
|
144
|
+
events: optionen.events,
|
|
145
|
+
description: optionen.description,
|
|
146
|
+
active: optionen.active,
|
|
147
|
+
}));
|
|
148
|
+
const secret = daten['secret'];
|
|
149
|
+
if (typeof secret !== 'string' || !secret) {
|
|
150
|
+
throw new errors_js_1.KasseneckValidationError('createPartnerWebhook', 'Antwort enthaelt kein secret — ohne es laesst sich keine Zustellung pruefen', 'response');
|
|
151
|
+
}
|
|
152
|
+
return { webhook: webhook(daten['webhook']), secret };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Ein neues Secret fuer denselben Endpunkt.
|
|
156
|
+
*
|
|
157
|
+
* Der Webhook behaelt seine `webhookId` und seine Ereignisse — nur das
|
|
158
|
+
* Geheimnis wechselt. Wer den Endpunkt stattdessen loescht und neu anlegt,
|
|
159
|
+
* verliert seine Zustellungshistorie und muss die Ereignisse neu ankreuzen.
|
|
160
|
+
*
|
|
161
|
+
* Das alte Secret gilt ab der Antwort NICHT mehr: die naechste Zustellung ist
|
|
162
|
+
* schon mit dem neuen signiert. Erst speichern, dann weiterarbeiten.
|
|
163
|
+
*/
|
|
164
|
+
async function rotatePartnerWebhookSecret(rufen, webhookId) {
|
|
165
|
+
const id = typeof webhookId === 'string' ? webhookId.trim() : '';
|
|
166
|
+
if (!id)
|
|
167
|
+
throw new errors_js_1.KasseneckValidationError('rotatePartnerWebhookSecret', 'webhookId fehlt', 'request');
|
|
168
|
+
const daten = objekt(await rufen('rotatePartnerWebhookSecret', { webhookId: id }));
|
|
169
|
+
const secret = daten['secret'];
|
|
170
|
+
if (typeof secret !== 'string' || !secret) {
|
|
171
|
+
throw new errors_js_1.KasseneckValidationError('rotatePartnerWebhookSecret', 'Antwort enthaelt kein secret — dann waere der Wechsel nicht nachvollziehbar', 'response');
|
|
172
|
+
}
|
|
173
|
+
return { webhook: webhook(daten['webhook']), secret };
|
|
174
|
+
}
|
|
175
|
+
/** Die Webhook-Endpunkte dieses Partners samt Ereignis-Katalog. */
|
|
176
|
+
async function listPartnerWebhooks(rufen) {
|
|
177
|
+
const daten = objekt(await rufen('listPartnerWebhooks'));
|
|
178
|
+
return {
|
|
179
|
+
webhooks: (Array.isArray(daten['webhooks']) ? daten['webhooks'] : []).map(webhook),
|
|
180
|
+
events: (Array.isArray(daten['events']) ? daten['events'] : []).map((e) => ({
|
|
181
|
+
key: typeof objekt(e)['key'] === 'string' ? objekt(e)['key'] : '',
|
|
182
|
+
text: typeof objekt(e)['text'] === 'string' ? objekt(e)['text'] : '',
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Aendert einen Endpunkt. Nur die genannten Felder — ein leeres `patch` lehnt
|
|
188
|
+
* das Backend ab (`validation`, Feld `patch`), statt stillschweigend nichts zu
|
|
189
|
+
* tun.
|
|
190
|
+
*/
|
|
191
|
+
async function updatePartnerWebhook(rufen, webhookId, patch) {
|
|
192
|
+
const id = typeof webhookId === 'string' ? webhookId.trim() : '';
|
|
193
|
+
if (!id)
|
|
194
|
+
throw new errors_js_1.KasseneckValidationError('updatePartnerWebhook', 'webhookId fehlt', 'request');
|
|
195
|
+
if (patch === null || typeof patch !== 'object' || Object.keys(patch).length === 0) {
|
|
196
|
+
throw new errors_js_1.KasseneckValidationError('updatePartnerWebhook', 'patch nennt keine Aenderung', 'request');
|
|
197
|
+
}
|
|
198
|
+
const daten = objekt(await rufen('updatePartnerWebhook', { webhookId: id, patch }));
|
|
199
|
+
return webhook(daten['webhook']);
|
|
200
|
+
}
|
|
201
|
+
/** Loescht einen Endpunkt. Danach kommt dort nichts mehr an. */
|
|
202
|
+
async function deletePartnerWebhook(rufen, webhookId) {
|
|
203
|
+
const id = typeof webhookId === 'string' ? webhookId.trim() : '';
|
|
204
|
+
if (!id)
|
|
205
|
+
throw new errors_js_1.KasseneckValidationError('deletePartnerWebhook', 'webhookId fehlt', 'request');
|
|
206
|
+
const daten = objekt(await rufen('deletePartnerWebhook', { webhookId: id }));
|
|
207
|
+
return typeof daten['webhookId'] === 'string' ? daten['webhookId'] : id;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Schickt eine Probe an genau diesen Endpunkt.
|
|
211
|
+
*
|
|
212
|
+
* Ohne `event` kommt `webhook.test` — der Nachweis, dass die Leitung steht und
|
|
213
|
+
* die eigene Signaturpruefung gegen echte Bytes laeuft. **Mit `event` kommt
|
|
214
|
+
* genau das Ereignis, das der Empfaenger behandeln soll**, mit einer
|
|
215
|
+
* glaubwuerdigen Nutzlast: wer auf `signature.ready` hin seinen Kunden
|
|
216
|
+
* benachrichtigt, probt das einmal, statt auf eine echte Karte zu warten. Eine
|
|
217
|
+
* Leitungsprobe beweist nichts ueber die Behandlung des Ernstfalls.
|
|
218
|
+
*
|
|
219
|
+
* Der Endpunkt muss das Ereignis abonnieren (`event_not_subscribed`) und aktiv
|
|
220
|
+
* sein (`webhook_inactive`); ein unbekannter Name ist ein `validation`-Fehler
|
|
221
|
+
* auf dem Feld `event`.
|
|
222
|
+
*
|
|
223
|
+
* **Jede Probe traegt `test: true` im Umschlag** ([PartnerWebhookEvent.test]).
|
|
224
|
+
*
|
|
225
|
+
* Der Backend-Endpunkt heisst `sendPartnerWebhookTest`; dieser Client behaelt
|
|
226
|
+
* den Namen bei, damit ein Leser der Doku und ein Leser des Codes dasselbe
|
|
227
|
+
* suchen.
|
|
228
|
+
*/
|
|
229
|
+
async function sendPartnerWebhookTest(rufen, webhookId, event) {
|
|
230
|
+
const id = typeof webhookId === 'string' ? webhookId.trim() : '';
|
|
231
|
+
if (!id)
|
|
232
|
+
throw new errors_js_1.KasseneckValidationError('sendPartnerWebhookTest', 'webhookId fehlt', 'request');
|
|
233
|
+
const ereignis = typeof event === 'string' ? event.trim() : '';
|
|
234
|
+
const daten = objekt(await rufen('sendPartnerWebhookTest', { webhookId: id, event: ereignis || undefined }));
|
|
235
|
+
return {
|
|
236
|
+
eventId: typeof daten['eventId'] === 'string' ? daten['eventId'] : '',
|
|
237
|
+
ereignis: typeof daten['ereignis'] === 'string' ? daten['ereignis'] : ereignis || 'webhook.test',
|
|
238
|
+
deliveries: Array.isArray(daten['deliveries']) ? daten['deliveries'] : [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Die letzten Zustellversuche — mit `webhookId` fuer einen Endpunkt, ohne ihn
|
|
243
|
+
* fuer alle. Die Stelle, an der sich „mein Server bekommt nichts" klaeren
|
|
244
|
+
* laesst, ohne bei Kasseneck nachzufragen.
|
|
245
|
+
*/
|
|
246
|
+
async function listPartnerWebhookDeliveries(rufen, optionen = {}) {
|
|
247
|
+
if (optionen.limit !== undefined && (!Number.isInteger(optionen.limit) || optionen.limit < 1 || optionen.limit > 200)) {
|
|
248
|
+
throw new errors_js_1.KasseneckValidationError('listPartnerWebhookDeliveries', 'limit muss zwischen 1 und 200 liegen', 'request');
|
|
249
|
+
}
|
|
250
|
+
const daten = objekt(await rufen('listPartnerWebhookDeliveries', { webhookId: optionen.webhookId, limit: optionen.limit }));
|
|
251
|
+
return (Array.isArray(daten['deliveries']) ? daten['deliveries'] : []).map((eintrag) => {
|
|
252
|
+
const z = objekt(eintrag);
|
|
253
|
+
const zahl = (wert) => (typeof wert === 'number' && Number.isFinite(wert) ? wert : null);
|
|
254
|
+
const txt = (wert) => (typeof wert === 'string' ? wert : null);
|
|
255
|
+
return {
|
|
256
|
+
deliveryId: txt(z['deliveryId']) ?? '',
|
|
257
|
+
webhookId: txt(z['webhookId']) ?? '',
|
|
258
|
+
event: txt(z['event']) ?? '',
|
|
259
|
+
eventId: txt(z['eventId']) ?? '',
|
|
260
|
+
status: txt(z['status']) ?? '',
|
|
261
|
+
attempts: zahl(z['attempts']) ?? 0,
|
|
262
|
+
letzterVersuchAt: zahl(z['letzterVersuchAt']),
|
|
263
|
+
naechsterVersuchAt: zahl(z['naechsterVersuchAt']),
|
|
264
|
+
statusCode: zahl(z['statusCode']),
|
|
265
|
+
response: txt(z['response']),
|
|
266
|
+
createdAt: zahl(z['createdAt']),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -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", "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>;
|
|
@@ -1,32 +1,51 @@
|
|
|
1
1
|
export const AUFRUFE = [
|
|
2
|
+
'activateCashregister',
|
|
2
3
|
'cancelReceipt',
|
|
4
|
+
'createCustomerCashregister',
|
|
5
|
+
'createPartnerCustomer',
|
|
6
|
+
'checkPartnerCustomerEmail',
|
|
7
|
+
'createPartnerWebhook',
|
|
3
8
|
'createPaymentLinkStripe',
|
|
4
9
|
'createPrintJob',
|
|
5
10
|
'createReceipt',
|
|
11
|
+
'deletePartnerWebhook',
|
|
6
12
|
'downloadDailyReport',
|
|
7
13
|
'downloadReport',
|
|
8
14
|
'endRegisterSession',
|
|
9
15
|
'financeWebService',
|
|
10
16
|
'generateFullReceiptId',
|
|
17
|
+
'getCustomerCredentials',
|
|
18
|
+
'getCustomerSignatureStatus',
|
|
11
19
|
'getFirstReceiptDate',
|
|
12
20
|
'getKasseSettings',
|
|
21
|
+
'getPartnerCustomer',
|
|
22
|
+
'getPartnerInfo',
|
|
13
23
|
'getPrintJob',
|
|
14
24
|
'getReceipt',
|
|
15
25
|
'hobexPayApi',
|
|
16
26
|
'hobexRefundApi',
|
|
27
|
+
'listCustomerCashregisters',
|
|
17
28
|
'listMyArticleGroups',
|
|
18
29
|
'listMyArticles',
|
|
19
30
|
'listMyCashregisters',
|
|
20
31
|
'listMyPrinters',
|
|
21
32
|
'listMyReceipts',
|
|
22
33
|
'listMyTipRecipients',
|
|
34
|
+
'listPartnerCustomers',
|
|
35
|
+
'listPartnerWebhookDeliveries',
|
|
36
|
+
'listPartnerWebhooks',
|
|
23
37
|
'listRegisterUsersForDevice',
|
|
24
38
|
'pairRegisterDevice',
|
|
25
39
|
'registerPinLogin',
|
|
26
40
|
'registerUserLogin',
|
|
27
41
|
'renewRegisterSession',
|
|
42
|
+
'requestCustomerSignature',
|
|
43
|
+
'sendPartnerCustomerFonLink',
|
|
44
|
+
'sendPartnerWebhookTest',
|
|
28
45
|
'setMyKasseSettings',
|
|
29
46
|
'setMyRegisterDeviceSettings',
|
|
30
47
|
'stripeCaptureIntent',
|
|
31
48
|
'unpairRegisterDevice',
|
|
49
|
+
'rotatePartnerWebhookSecret',
|
|
50
|
+
'updatePartnerWebhook',
|
|
32
51
|
];
|
|
@@ -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';
|