/** * Webhooks: Endpunkte verwalten und eingehende Zustellungen auswerten. * * Die Signaturpruefung liegt daneben in `webhook-signatur.ts` — sie braucht * weder Transport noch Schluessel und laeuft in jedem Empfaenger, auch in * einem, der sonst nichts von diesem Paket benutzt. */ import type { InternerTransport } from '../client/aufrufe.js'; import type { CashregisterActivationStep, ContractKind, ContractSource, PartnerEnv } from './typen.js'; import { type VerifyWebhookOptions, type WebhookVerifyReason } from './webhook-signatur.js'; /** * Alle Ereignisse, die ein Webhook abonnieren **und proben** kann, in der * Reihenfolge des Backend-Katalogs (`listPartnerWebhooks().events`). Ein * Endpunkt bekommt ausschliesslich die, die in seiner `events`-Liste stehen. * * Die Nutzlasten der Ereignisse, die `/v3` uebersetzt, beschreiben * [ContractAcceptedEventData], [SignatureFailedEventData] und * [CashregisterFailedEventData]. */ export declare const PARTNER_WEBHOOK_EVENTS: readonly ["customer.created", "customer.updated", "customer.status_changed", "customer.fon_verified", "customer.live_enabled", "customer.avv_accepted", "customer.terms_accepted", "signature.requested", "signature.ready", "signature.failed", "cashregister.created", "cashregister.live", "cashregister.failed", "app.version.accepted", "app.version.rejected", "webhook.test"]; export type PartnerWebhookEventType = typeof PARTNER_WEBHOOK_EVENTS[number]; export declare function isPartnerWebhookEvent(value: unknown): value is PartnerWebhookEventType; /** * Die Felder des Umschlags, so wie er auf der Leitung liegt. * * Als Liste, damit der Zwilling sie nachhaelt: `test` kam spaeter dazu, und * genau ein solches Feld verschwindet sonst auf einer Seite, ohne dass etwas * rot wird. */ export declare const WEBHOOK_ENVELOPE_FIELDS: readonly ["id", "type", "createdAt", "partnerId", "test", "data"]; /** * Die Huelle jeder Zustellung. `type` bleibt bewusst offen fuer unbekannte * Werte (`(string & {})`): ein spaeter ergaenztes Ereignis soll einen * Empfaenger nicht zum Absturz bringen, sondern in seinem `default`-Zweig * landen. */ export interface PartnerWebhookEvent> { /** `evt_…` — die Kennung, auf die **entdoppelt** wird. */ id: string; type: PartnerWebhookEventType | (string & {}); createdAt: number; partnerId: string; /** * **Probe oder Ernstfall.** Eine mit [sendPartnerWebhookTest] ausgeloeste * Zustellung traegt `test: true` im Umschlag; ein echtes Ereignis fuehrt das * Feld gar nicht, hier steht dann `false`. * * Diese Zeile gehoert an den Anfang jedes Handlers: * * ```ts * if (ereignis.test) return; * ``` * * Ohne sie haelt jemand eine Probe fuer echt und schreibt seinem Kunden, die * Kasse sei fertig. Eine Probe traegt eine erkennbar erfundene Nutzlast — nur * sieht man das erst, wenn man hinsieht. */ test: boolean; data: T; } /** * Die Sprache der Nutzlast eines Webhooks. Ein unter `/v1` angelegter Webhook * spricht `v1` (deutsche Werte), ein unter `/v3` angelegter `v3`. Umstellen * geht nur vorwaerts, mit `updatePartnerWebhook(id, { apiVersion: 'v3' })`; * zurueck auf `v1` gibt es absichtlich nicht. * * Die Sprache der **Antworten** folgt dagegen dem Pfad: dieser Client ruft * `/v3` und bekommt englische Antworten, gleich welche `apiVersion` ein * Webhook hat. */ export type WebhookApiVersion = 'v1' | 'v3'; /** * Nutzlast von `customer.avv_accepted` und `customer.terms_accepted` in der * Sprache `v3`. Ein Webhook mit `apiVersion: 'v1'` schickt hier weiter * `kind: 'nutzung'` und die deutschen Quellen. */ export interface ContractAcceptedEventData { customerId: string; companyName: string; kind: ContractKind; version: string; confirmedAt: number; source: ContractSource; } /** * Nutzlast von `signature.failed` in der Sprache `v3`. `code` ist immer * `signature_failed` (FinanzOnline hat die Registrierung abgelehnt, `rc` * nennt den Rueckgabecode); die Ursache im Einzelnen steht am Antrag * (`getCustomerSignatureStatus`, `requests[].error.code`, siehe * [SIGNATURE_ERROR_CODES]). `message` ist ein Text fuer Menschen. */ export interface SignatureFailedEventData { customerId: string; companyName: string; requestId: string; code: 'signature_failed' | (string & {}); /** Rueckgabecode von FinanzOnline, falls es einen gab. */ rc: string | null; message: string; } /** * Nutzlast von `cashregister.failed` in der Sprache `v3`: an welchem Schritt * die Inbetriebnahme stehen blieb und mit welchem Code. Der Server feuert das * Ereignis mit `signature_not_ready`, `fon_missing` oder `activation_failed`. * Fehlende Vertraege (`contracts_pending`) loesen **kein** Ereignis aus: das * meldet `activateCashregister` als Fehler, die Freigabe melden * `customer.avv_accepted` und `customer.terms_accepted`. */ export interface CashregisterFailedEventData { customerId: string; companyName: string; cashregisterId: string; step: CashregisterActivationStep | null; code: 'signature_not_ready' | 'fon_missing' | 'activation_failed' | (string & {}); /** Rueckgabecode von FinanzOnline, falls es einen gab. */ rc: string | null; message: string; env: PartnerEnv; } export type WebhookEventResult = { ok: true; event: PartnerWebhookEvent; timestampSec: number; } | { ok: false; reason: WebhookVerifyReason | 'body-not-json' | 'body-not-event'; }; /** * Prueft die Signatur **und** liest das Ereignis — in dieser Reihenfolge. Wer * zuerst parst und dann prueft, hat den fremden Rumpf schon durch seinen Code * laufen lassen. * * **Entdoppeln nicht vergessen:** dieselbe Zustellung kann mehrfach ankommen * (Wiederholung nach einer Antwort, die unterwegs verloren ging). Massgeblich * ist `event.id`, nicht der Kopf `X-Kasseneck-Delivery` — der ist bei * Wiederholungen derselbe, aber er beantwortet die Frage "habe ich dieses * Ereignis schon verarbeitet?" nur fuer genau diesen Endpunkt. * * Antworte innerhalb von 10 Sekunden mit 2xx und erledige die Arbeit danach. * Eine ausbleibende Antwort wird bis zu fuenfmal wiederholt (1 min, 5 min, * 30 min, 2 h, 12 h) und gilt dann als fehlgeschlagen. */ export declare function parseWebhookEvent(options: VerifyWebhookOptions): Promise; /** Staende einer Zustellung; `/v1` sagte `offen`, `zugestellt`, `fehlgeschlagen`, `verworfen`. */ export declare const WEBHOOK_DELIVERY_STATUSES: readonly ["pending", "delivered", "failed", "dropped"]; export type WebhookDeliveryStatus = typeof WEBHOOK_DELIVERY_STATUSES[number]; export interface PartnerWebhook { webhookId: string; /** Sprache der Nutzlast; fehlt sie in der Antwort, ist es ein Bestands-Webhook und damit `v1`. */ apiVersion: WebhookApiVersion | (string & {}); url: string; events: string[]; active: boolean; description: string | null; createdAt: number | null; /** Die letzte Zustellung; `null`, solange keine versucht wurde. */ lastDelivery: { at: number | null; status: WebhookDeliveryStatus | (string & {}); statusCode: number | null; } | null; /** Fehlversuche in Folge — steigt der Wert, stimmt beim Empfaenger etwas nicht. */ consecutiveFailures: number; } export interface CreateWebhookOptions { /** Vollstaendige `https://`-Adresse. */ url: string; /** Mindestens eines; unbekannte Namen lehnt das Backend ab. */ events: (PartnerWebhookEventType | (string & {}))[]; /** Hoechstens 120 Zeichen. */ description?: string; active?: boolean; } export interface CreateWebhookResult { webhook: PartnerWebhook; /** * Das Secret fuer die Signaturpruefung. **Es kommt genau einmal** — beim * Anlegen. Danach gibt das Backend es nie wieder aus; wer es verliert, legt * einen neuen Endpunkt an. * * Bewusst ein `string` und kein [KasseneckSecret]: es gehoert dem Partner * selbst und nicht einem fremden Betrieb, und es muss unveraendert in die * eigene Konfiguration wandern. Verschluesselt speichern gilt trotzdem. */ secret: string; } export interface WebhookPatch { url?: string; events?: (PartnerWebhookEventType | (string & {}))[]; description?: string; active?: boolean; /** * Stellt die Nutzlast auf Englisch um. Nur `'v3'` ist moeglich: zurueck auf * `v1` geht nicht, damit ein Partner nicht versehentlich wieder deutsche * Nutzlasten bekommt. */ apiVersion?: 'v3'; } export interface DeleteWebhookResult { webhookId: string; /** Unter `/v1` hiess das Feld `geloescht`. */ deleted: boolean; } export interface WebhookList { webhooks: PartnerWebhook[]; /** Der Katalog: Ereignisname und deutscher Text, so wie das Panel ihn zeigt. */ events: { key: string; text: string; }[]; } export interface WebhookDelivery { deliveryId: string; webhookId: string; event: string; eventId: string; /** `dropped`: der Webhook wurde vor der Faelligkeit deaktiviert oder geloescht. */ status: WebhookDeliveryStatus | (string & {}); attempts: number; lastAttemptAt: number | null; nextAttemptAt: number | null; statusCode: number | null; /** Auszug der Antwort des Empfaengers, hoechstens 500 Zeichen. */ response: string | null; createdAt: number | null; } /** * Legt einen Webhook-Endpunkt an. Hoechstens [WEBHOOK_LIMIT] je Partner * (`webhook_limit`). * * **Das Secret in der Antwort ist der einzige Weg dazu.** Es sofort dorthin * schreiben, wo der Empfaenger es liest — nicht in ein Protokoll. */ export declare function createPartnerWebhook(transport: InternerTransport, options: CreateWebhookOptions): Promise; /** * Ein neues Secret fuer denselben Endpunkt. * * Der Webhook behaelt seine `webhookId` und seine Ereignisse — nur das * Geheimnis wechselt. Wer den Endpunkt stattdessen loescht und neu anlegt, * verliert seine Zustellungshistorie und muss die Ereignisse neu ankreuzen. * * Das alte Secret gilt ab der Antwort NICHT mehr: die naechste Zustellung ist * schon mit dem neuen signiert. Erst speichern, dann weiterarbeiten. */ export declare function rotatePartnerWebhookSecret(transport: InternerTransport, webhookId: string): Promise; /** Die Webhook-Endpunkte dieses Partners samt Ereignis-Katalog. */ export declare function listPartnerWebhooks(transport: InternerTransport): Promise; /** * Aendert einen Endpunkt. Nur die genannten Felder — ein leeres `patch` lehnt * das Backend ab (`validation`, Feld `patch`), statt stillschweigend nichts zu * tun. */ export declare function updatePartnerWebhook(transport: InternerTransport, webhookId: string, patch: WebhookPatch): Promise; /** * Loescht einen Endpunkt. Danach kommt dort nichts mehr an; offene * Zustellungen werden verworfen (`dropped`). */ export declare function deletePartnerWebhook(transport: InternerTransport, webhookId: string): Promise; /** Eine Zustellung der Probe, so wie `sendPartnerWebhookTest` sie meldet. */ export interface WebhookTestDelivery { deliveryId: string; webhookId: string; status: WebhookDeliveryStatus | (string & {}); statusCode: number | null; } export interface WebhookTestResult { eventId: string; /** Welches Ereignis geprobt wurde — ohne Angabe `webhook.test`. */ event: string; deliveries: WebhookTestDelivery[]; } /** * Schickt eine Probe an genau diesen Endpunkt. * * Ohne `event` kommt `webhook.test` — der Nachweis, dass die Leitung steht und * die eigene Signaturpruefung gegen echte Bytes laeuft. **Mit `event` kommt * genau das Ereignis, das der Empfaenger behandeln soll**, mit einer * glaubwuerdigen Nutzlast: wer auf `signature.ready` hin seinen Kunden * benachrichtigt, probt das einmal, statt auf eine echte Karte zu warten. Eine * Leitungsprobe beweist nichts ueber die Behandlung des Ernstfalls. * * Der Endpunkt muss das Ereignis abonnieren (`event_not_subscribed`) und aktiv * sein (`webhook_inactive`); ein unbekannter Name ist ein `validation`-Fehler * auf dem Feld `event`. * * **Jede Probe traegt `test: true` im Umschlag** ([PartnerWebhookEvent.test]). * * Der Backend-Endpunkt heisst `sendPartnerWebhookTest`; dieser Client behaelt * den Namen bei, damit ein Leser der Doku und ein Leser des Codes dasselbe * suchen. */ export declare function sendPartnerWebhookTest(transport: InternerTransport, webhookId: string, event?: PartnerWebhookEventType | (string & {})): Promise; /** * Die letzten Zustellversuche — mit `webhookId` fuer einen Endpunkt, ohne ihn * fuer alle. Die Stelle, an der sich „mein Server bekommt nichts" klaeren * laesst, ohne bei Kasseneck nachzufragen. */ export declare function listPartnerWebhookDeliveries(transport: InternerTransport, options?: { webhookId?: string; limit?: number; }): Promise;