PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / resources / js / api / webhooks-api.ts

webhooks-api.ts in Yatra – Travel Booking & Tour Operator Software trunk, at resources/js/api/webhooks-api.ts

337 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { apiClient } from "../lib/api-client";
2
3 /* -------------------------------------------------------------------------- */
4 /* Types */
5 /* -------------------------------------------------------------------------- */
6
7 /** Module-level gate state, drives the Webhooks page upgrade / setup cards. */
8 export interface WebhooksMeta {
9 is_agency_active: boolean;
10 is_module_enabled: boolean;
11 api_version: string;
12 upgrade_url: string;
13 docs_url: string;
14 signature_header: string;
15 event_count: number;
16 }
17
18 /** One row from the event catalog — what the operator can subscribe to. */
19 export interface WebhookEvent {
20 key: string;
21 name: string;
22 description: string;
23 wp_hook: string;
24 entity: string;
25 }
26
27 /** An operator-configured destination URL. */
28 export interface WebhookEndpoint {
29 id: number;
30 name: string;
31 url: string;
32 description: string;
33 /** Exactly ONE event per endpoint (1:1 mapping, mirrors the Email
34 * Automation pattern). The field selector + sample payload viewer
35 * show data for THIS event only — multi-event endpoints would make
36 * field selection ambiguous. */
37 event: string;
38 /** Outbound HTTP verb. POST is the universal webhook default;
39 * PUT/PATCH suit upsert/partial-update receivers; DELETE for
40 * object-removal mirrors; GET moves the payload to a
41 * `?payload=<json>` query parameter and the body becomes empty
42 * (so the HMAC signature is then over `timestamp + '.'` — i.e.
43 * empty body). Defaults to POST. */
44 http_method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET";
45 /** Static custom HTTP headers POSTed alongside Yatra's signed headers.
46 * Reserved header names (`X-Yatra-*`, Content-Length, Host, Cookie)
47 * are blocked server-side. */
48 headers: Record<string, string>;
49 /** Static fields merged into every payload's `data` block — e.g.
50 * `{tenant_id: "acme", environment: "production"}`. Operator-extras
51 * never overwrite Yatra's canonical entity fields. */
52 additional_payload_fields: Record<string, string>;
53 /** Pabbly-style field filter. Empty list = "send the entire data
54 * block". Populated = "send only these dot-paths". The envelope
55 * (id/type/api_version/occurred_at) is ALWAYS preserved regardless
56 * — receivers route on those. */
57 selected_fields: string[];
58 secret_hint: string;
59 is_active: boolean;
60 /** When false, the dispatcher persists delivery metadata (status,
61 * attempts, duration) but strips the payload + response bodies.
62 * High-volume endpoints turn this off to control disk usage. */
63 log_deliveries: boolean;
64 consecutive_failures: number;
65 last_delivered_at: string | null;
66 last_status: string | null;
67 created_at: string;
68 updated_at: string;
69 /** Rolling success-rate snapshot over the last 100 terminal-state
70 * deliveries. `success_rate` is null when no deliveries exist yet. */
71 health: {
72 total: number;
73 delivered: number;
74 failed: number;
75 success_rate: number | null;
76 };
77 }
78
79 /**
80 * Create / update payload — same as WebhookEndpoint but with the
81 * write-only `custom_secret` field. The server NEVER reads this back
82 * (it lives only in the encrypted SecretStore), so it's omitted from
83 * WebhookEndpoint itself.
84 */
85 export type WebhookEndpointWriteInput = Partial<WebhookEndpoint> & {
86 /** Optional. If provided (min 24 chars), uses this as the signing
87 * secret instead of auto-generating one. Useful when the receiver
88 * already has a secret configured. */
89 custom_secret?: string;
90 };
91
92 /** Lightweight delivery log row for the list view. */
93 export interface WebhookDeliveryRow {
94 id: number;
95 endpoint_id: number;
96 event_key: string;
97 delivery_id: string;
98 http_status: number | null;
99 attempts: number;
100 status:
101 | "queued"
102 | "delivering"
103 | "delivered"
104 | "failed"
105 | "permanent_failure";
106 duration_ms: number | null;
107 error_message: string | null;
108 created_at: string;
109 delivered_at: string | null;
110 next_attempt_at: string | null;
111 }
112
113 /** Full delivery detail including the canonical payload — used by the inspect view. */
114 export interface WebhookDeliveryDetail extends WebhookDeliveryRow {
115 payload: Record<string, unknown>;
116 response_body: string | null;
117 response_headers: string | null;
118 }
119
120 /** Real-time capture state for one event key. Polled by the form while
121 * the operator is waiting for a sample. */
122 export interface ListenStatus {
123 armed: boolean;
124 expires_at: number | null;
125 captured: {
126 captured_at: number;
127 payload: Record<string, unknown>;
128 paths: Array<{ path: string; sample: unknown }>;
129 } | null;
130 }
131
132 /* -------------------------------------------------------------------------- */
133 /* Client */
134 /* -------------------------------------------------------------------------- */
135
136 export const webhooksApi = {
137 getMeta: () => apiClient.get("/webhooks/meta") as Promise<WebhooksMeta>,
138
139 listEvents: () =>
140 apiClient.get("/webhooks/events") as Promise<{ data: WebhookEvent[] }>,
141
142 /** Returns the latest REAL payload for this event — either explicitly
143 * captured via {@link startListen} or pulled from a prior delivery.
144 * `payload` is null if no sample exists yet (UI prompts to Listen). */
145 getEventSample: (key: string) =>
146 apiClient.get(
147 `/webhooks/events/${encodeURIComponent(key)}/sample`,
148 ) as Promise<{
149 event: WebhookEvent;
150 payload: Record<string, unknown> | null;
151 paths: Array<{ path: string; sample: unknown }>;
152 /** "captured" (operator-driven listen) | "delivery_log" (prior real send) | null. */
153 source: "captured" | "delivery_log" | null;
154 captured_at: number | null;
155 }>,
156
157 /** Real-time capture flow (Pabbly/Zapier-style "listen for sample"):
158 * arm a capture, the next firing of the event records its real
159 * payload, the UI polls until it shows up. Zero guesswork. */
160 startListen: (key: string) =>
161 apiClient.post(
162 `/webhooks/events/${encodeURIComponent(key)}/listen`,
163 {},
164 ) as Promise<ListenStatus>,
165
166 getListenStatus: (key: string) =>
167 apiClient.get(
168 `/webhooks/events/${encodeURIComponent(key)}/listen`,
169 ) as Promise<ListenStatus>,
170
171 /** Cancel an active capture. Pass forget=true to also discard the
172 * previously-captured sample (gives a clean slate). */
173 stopListen: (key: string, forget = false) =>
174 apiClient.delete(
175 `/webhooks/events/${encodeURIComponent(key)}/listen${forget ? "?forget=1" : ""}`,
176 ) as Promise<ListenStatus>,
177
178 listEndpoints: () =>
179 apiClient.get("/webhooks/endpoints") as Promise<{
180 data: WebhookEndpoint[];
181 }>,
182
183 getEndpoint: (id: number) =>
184 apiClient.get(`/webhooks/endpoints/${id}`) as Promise<{
185 data: WebhookEndpoint;
186 }>,
187
188 /** Returns the plaintext signing secret ONCE — show in a copy dialog
189 * unless the operator provided a custom_secret (in which case they
190 * already know it; the server still echoes it back for confirmation). */
191 createEndpoint: (payload: WebhookEndpointWriteInput) =>
192 apiClient.post("/webhooks/endpoints", payload) as Promise<{
193 data: WebhookEndpoint;
194 secret: string;
195 message: string;
196 }>,
197
198 updateEndpoint: (id: number, payload: WebhookEndpointWriteInput) =>
199 apiClient.put(`/webhooks/endpoints/${id}`, payload) as Promise<{
200 data: WebhookEndpoint;
201 message: string;
202 }>,
203
204 deleteEndpoint: (id: number) =>
205 apiClient.delete(`/webhooks/endpoints/${id}`) as Promise<{
206 message: string;
207 }>,
208
209 /** Generates a fresh secret — shown ONCE. Invalidates the previous one. */
210 regenerateSecret: (id: number) =>
211 apiClient.post(
212 `/webhooks/endpoints/${id}/regenerate-secret`,
213 {},
214 ) as Promise<{
215 secret: string;
216 message: string;
217 }>,
218
219 /** Queues a synthetic `webhook.ping` event — operator inspects the result
220 * in the Deliveries tab afterwards. */
221 pingEndpoint: (id: number) =>
222 apiClient.post(`/webhooks/endpoints/${id}/ping`, {}) as Promise<{
223 delivery_id: string;
224 row_id: number;
225 message: string;
226 }>,
227
228 listDeliveries: (
229 params: {
230 page?: number;
231 per_page?: number;
232 endpoint_id?: number;
233 event_key?: string;
234 status?: string;
235 } = {},
236 ) =>
237 apiClient.get("/webhooks/deliveries", { params }) as Promise<{
238 data: WebhookDeliveryRow[];
239 total: number;
240 page: number;
241 per_page: number;
242 }>,
243
244 /** Returns the FULL payload — the "listen mode" view. */
245 getDelivery: (id: number) =>
246 apiClient.get(`/webhooks/deliveries/${id}`) as Promise<{
247 data: WebhookDeliveryDetail;
248 }>,
249
250 /** Re-queues a delivery for a fresh attempt. Attempt counter preserved. */
251 replayDelivery: (id: number) =>
252 apiClient.post(`/webhooks/deliveries/${id}/replay`, {}) as Promise<{
253 message: string;
254 }>,
255
256 /**
257 * Bulk replay. Accepts either explicit ids[] (cap 200) or a filter
258 * descriptor (cap 500). Rows whose endpoint is inactive or deleted
259 * are skipped and counted separately so the UI can show a partial-
260 * success summary.
261 */
262 bulkReplayDeliveries: (input: {
263 ids?: number[];
264 filter?: {
265 endpoint_id?: number;
266 event_key?: string;
267 status?: "failed" | "permanent_failure" | "delivered";
268 before?: string;
269 after?: string;
270 };
271 }) =>
272 apiClient.post("/webhooks/deliveries/bulk-replay", input) as Promise<{
273 requeued: number;
274 skipped: number;
275 skipped_inactive_endpoint?: number;
276 skipped_missing?: number;
277 /** When the filter matched more than the 500-row cap, this is
278 * the total match count so the UI can suggest a narrower filter. */
279 capped_total: number | null;
280 message: string;
281 }>,
282
283 /**
284 * Aggregated buried-deliveries snapshot. Pairs with bulkReplay above
285 * — operators triage from this summary, then bulk-replay either by
286 * explicit ids picked from `recent[]` or by filter (e.g. "every
287 * permanent_failure for endpoint 7 from the last 24h").
288 */
289 getDeadLetterSummary: () =>
290 apiClient.get("/webhooks/deliveries/dead-letter") as Promise<{
291 data: {
292 total: number;
293 by_endpoint: Array<{ endpoint_id: number; count: number }>;
294 by_event: Array<{ event_key: string; count: number }>;
295 /** Error-message prefix grouping — clusters HTTP 503 / timeout
296 * variants so the operator sees the pattern, not 1000 rows. */
297 by_error: Array<{ fingerprint: string; count: number }>;
298 recent: WebhookDeliveryRow[];
299 };
300 }>,
301
302 /* ----------------------------- mTLS ------------------------------ */
303 /** Per-endpoint client-cert state. Returns `configured: false` when
304 * nothing's been uploaded. Never returns the key/cert PEM — only
305 * fingerprint + expiry hint. */
306 getMtlsHint: (id: number) =>
307 apiClient.get(`/webhooks/endpoints/${id}/mtls`) as Promise<{
308 data: {
309 configured: boolean;
310 fingerprint: string;
311 expires_at: string | null;
312 };
313 }>,
314
315 /** Upload a PEM cert + private key (+ optional passphrase). The
316 * server validates the pair matches with openssl_x509_check_private_key
317 * before persisting, so a mismatched-pair mistake is caught at save
318 * time rather than at delivery time. */
319 setMtls: (
320 id: number,
321 payload: { cert: string; key: string; passphrase?: string },
322 ) =>
323 apiClient.post(`/webhooks/endpoints/${id}/mtls`, payload) as Promise<{
324 data: {
325 configured: boolean;
326 fingerprint: string;
327 expires_at: string | null;
328 };
329 message: string;
330 }>,
331
332 clearMtls: (id: number) =>
333 apiClient.delete(`/webhooks/endpoints/${id}/mtls`) as Promise<{
334 message: string;
335 }>,
336 };
337