sabre-client 0.2.3 → 0.3.0-next.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Carlos Ivan Soto Perez
3
+ Copyright (c) 2026 Carlos Ivan Soto Perez
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.es.md ADDED
@@ -0,0 +1,264 @@
1
+ # sabre-client
2
+
3
+ SDK de TypeScript para las APIs de Sabre.
4
+
5
+ [Read in English](./README.md)
6
+
7
+ > **`0.3.0` es una reescritura completa, no una actualización incremental de `0.2.4`.**
8
+ > Tiene otra arquitectura: un cliente REST `Sabre` con un `AuthManager` que gestiona el token,
9
+ > una jerarquía de errores tipada, un logger inyectable con redacción obligatoria y solo Web
10
+ > Standard APIs, para correr en cualquier runtime de servidor. El código escrito para `0.2.4`
11
+ > no funciona sin cambios. Las versiones anteriores siguen publicadas tal como están.
12
+
13
+ ## Estado
14
+
15
+ `0.3.0` **todavía no está publicada**. Se publica cuando estén terminados el Tier 1 y el
16
+ Tier 2 del flujo de Air Booking. Mientras tanto, `npm install sabre-client` instala `0.2.4`.
17
+
18
+ Las prereleases de la reescritura se publican con el tag `next`:
19
+
20
+ ```sh
21
+ npm install sabre-client@next
22
+ ```
23
+
24
+ La API todavía puede cambiar antes de `0.3.0`.
25
+
26
+ El Tier 1 cubre el flujo básico de Air Booking por REST:
27
+
28
+ | Paso | Servicio | Método |
29
+ | - | - | - |
30
+ | Autenticación | OAuth Token Create v3 / v2 | automática |
31
+ | Búsqueda | Bargain Finder Max v5 | `sabre.air.bargainFinderMax` |
32
+ | Revalidación | Revalidate Itinerary v5 | `sabre.air.revalidateItinerary` |
33
+ | Reserva | Create Booking (Booking Management API v1, ATPCO por `flightDetails`) | `sabre.booking.create` |
34
+ | Gestión de la reserva | Get Booking y Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
35
+
36
+ Planeado: Get Seats y Get Ancillaries (Tier 2), y un cliente `SabreLegacy` para los servicios SOAP.
37
+
38
+ ## Requisitos
39
+
40
+ - Node.js `>=22.12`, o un runtime edge de servidor con `fetch` y `crypto.subtle`
41
+ (Cloudflare Workers, Vercel Edge Functions, Deno Deploy). El paquete usa solo Web Standard
42
+ APIs.
43
+ - Solo ESM.
44
+ - **Nunca lo corras en un navegador.** Tus credenciales de Sabre quedarían expuestas a
45
+ cualquiera con las devtools.
46
+
47
+ ## Configuración
48
+
49
+ El SDK nunca lee variables de entorno. La configuración se pasa explícitamente:
50
+
51
+ ```ts
52
+ import { Sabre } from 'sabre-client';
53
+
54
+ const sabre = new Sabre({
55
+ environment: 'sandbox', // 'sandbox' (CERT) o 'production'
56
+ credentials: {
57
+ clientId: process.env.SABRE_CLIENT_ID!,
58
+ clientSecret: process.env.SABRE_CLIENT_SECRET!,
59
+ username: process.env.SABRE_EPR_USERNAME!, // usuario EPR, p. ej. `<user>-<PCC>-AA`
60
+ password: process.env.SABRE_EPR_PASSWORD!,
61
+ },
62
+ });
63
+ ```
64
+
65
+ | Opción | Default | Descripción |
66
+ | - | - | - |
67
+ | `environment` | requerido | `'sandbox'` → `api.cert.platform.sabre.com`, `'production'` → `api.platform.sabre.com` |
68
+ | `authVersion` | `'v3'` | Versión de OAuth Token Create. `'v3'` necesita `clientId`, `clientSecret`, `username` y `password`; `'v2'` solo el `username` EPR (`V1:<user>:<PCC>:AA`) y el `password` |
69
+ | `credentials` | requerido | Ver arriba |
70
+ | `logger` | ninguno | Cualquier objeto con `debug`, `info`, `warn` y `error` (ver [Logging](#logging)) |
71
+ | `retry` | `{ maxRetries: 3, baseDelayMs: 500 }` | Política de reintentos del cliente; cada request puede pisarla |
72
+ | `fetch` | `fetch` global | `fetch` propio (proxies, tests, otros runtimes) |
73
+ | `tokenStore` | solo en memoria | Comparte el token entre procesos (ver [Reutilizar el token](#reutilizar-el-token)) |
74
+
75
+ ### Autenticación
76
+
77
+ Nunca pides el token tú. El cliente lo obtiene en la primera llamada, lo cachea, lo renueva
78
+ alrededor de una hora antes de que expire y comparte una sola petición en vuelo entre llamadas
79
+ concurrentes. Si Sabre rechaza un token con `401`, el cliente obtiene uno nuevo y reintenta esa
80
+ request una vez.
81
+
82
+ ## Reservar un vuelo
83
+
84
+ El flujo completo del Tier 1: buscar, revalidar el itinerario elegido y reservarlo.
85
+
86
+ ```ts
87
+ import { Sabre, buildRevalidateItineraryRequest, buildBookingFlightDetails } from 'sabre-client';
88
+
89
+ const sabre = new Sabre({ environment: 'sandbox', credentials });
90
+
91
+ // 1. Buscar con Bargain Finder Max. El request es el body que documenta Sabre.
92
+ const search = {
93
+ OTA_AirLowFareSearchRQ: {
94
+ Version: '5',
95
+ POS: { Source: [{ PseudoCityCode: 'XXXX', RequestorID: { Type: '1', ID: '1', CompanyName: { Code: 'TN' } } }] },
96
+ OriginDestinationInformation: [
97
+ { DepartureDateTime: '2026-12-01T10:00:00', OriginLocation: { LocationCode: 'DFW' }, DestinationLocation: { LocationCode: 'LAX' } },
98
+ ],
99
+ TravelerInfoSummary: { AirTravelerAvail: [{ PassengerTypeQuantity: [{ Code: 'ADT', Quantity: 1 }] }] },
100
+ TPA_Extensions: { IntelliSellTransaction: { RequestType: { Name: '50ITINS' } } },
101
+ },
102
+ };
103
+ const found = await sabre.air.bargainFinderMax(search);
104
+
105
+ // Las respuestas llegan con las referencias ya resueltas: cada itinerario trae sus tramos,
106
+ // horarios y tarifas en línea, sin tener que buscar por `ref`.
107
+ const group = found.itineraryGroups[0]!;
108
+ const itinerary = group.itineraries[0]!;
109
+
110
+ // 2. Revalidar el itinerario elegido (disponibilidad y precio, sin retener inventario).
111
+ const revalidated = await sabre.air.revalidateItinerary(
112
+ buildRevalidateItineraryRequest({ search, group, itinerary }),
113
+ );
114
+ const confirmedGroup = revalidated.itineraryGroups[0]!;
115
+ const confirmed = confirmedGroup.itineraries[0]!;
116
+
117
+ // 3. Reservarlo por el camino ATPCO (`flightDetails`).
118
+ const booking = await sabre.booking.create({
119
+ flightDetails: buildBookingFlightDetails({ group: confirmedGroup, itinerary: confirmed }),
120
+ travelers: [{ givenName: 'JANE', surname: 'DOE', passengerCode: 'ADT' }],
121
+ contactInfo: { emails: ['jane@example.com'], phones: ['5550000000'] },
122
+ });
123
+
124
+ console.log(booking.confirmationId); // localizador del PNR
125
+ ```
126
+
127
+ `buildBookingFlightDetails` convierte el itinerario revalidado en el bloque `flightDetails` de
128
+ Create Booking. Sabre no documenta ese mapeo y todavía no se comprobó con una reserva real en
129
+ sandbox, así que revisa el request antes de depender de él.
130
+
131
+ `sabre.booking.create` **nunca se reintenta automáticamente**, porque no es idempotente. Si
132
+ Sabre crea la reserva con avisos o errores no fatales, la llamada no lanza: revisa
133
+ `booking.warnings` y `booking.errors`.
134
+
135
+ `sabre.booking.get` lee una reserva por `confirmationId`. A diferencia de Create Booking, la
136
+ respuesta trae los campos de la reserva en el primer nivel (`bookingId`, `flights`,
137
+ `travelers`, …), tal como la define Sabre. Usa `returnOnly` para pedir solo las secciones que
138
+ necesitas; además, la llamada es más rápida:
139
+
140
+ ```ts
141
+ const pnr = await sabre.booking.get({
142
+ confirmationId: booking.confirmationId,
143
+ returnOnly: ['FLIGHTS', 'TRAVELERS'],
144
+ });
145
+ ```
146
+
147
+ Solo lanza `SabreBusinessError` si no hay reserva (por ejemplo, `BOOKING_NOT_FOUND`). Si la
148
+ reserva existe, los problemas parciales vuelven en `pnr.warnings` y `pnr.errors`; una reserva
149
+ sin emitir suele traer el aviso `RESOURCE_UNAVAILABLE` porque todavía no tiene fare rules. Es
150
+ una lectura, así que los fallos transitorios se reintentan.
151
+
152
+ `sabre.booking.cancel` cancela la reserva entera (`cancelAll: true`) o productos concretos por
153
+ el `itemId` que devuelve `get`:
154
+
155
+ ```ts
156
+ await sabre.booking.cancel({ confirmationId: booking.confirmationId, cancelAll: true });
157
+ await sabre.booking.cancel({ confirmationId: pnr.bookingId, flights: [{ itemId: '2' }] });
158
+ ```
159
+
160
+ Como `create`, **nunca se reintenta automáticamente**. Las combinaciones que Sabre siempre
161
+ rechaza (`cancelAll` junto con listas, ni `cancelAll` ni listas, `offerItemId` junto con
162
+ `flightTicketOperation`) lanzan un `TypeError` antes de enviar nada. Si Sabre responde
163
+ `NO_ITEMS_CANCELLED`, la llamada lanza `SabreBusinessError` aunque Sabre lo mande como aviso,
164
+ porque no se canceló nada; los errores que explican el motivo están en `error.errors`. El aviso
165
+ `END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica el resultado
166
+ con `sabre.booking.get`.
167
+
168
+ Toda request acepta un `AbortSignal`. Las búsquedas aceptan además una política de reintentos
169
+ propia:
170
+
171
+ ```ts
172
+ await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
173
+ ```
174
+
175
+ ## Manejo de errores
176
+
177
+ Todos los errores que lanza el SDK extienden `SabreError`:
178
+
179
+ | Clase | Cuándo | Campos extra |
180
+ | - | - | - |
181
+ | `SabreGatewayError` | Falla HTTP o del gateway (`ERR.2SG.*`, `401`, `429`, `5xx`, errores de OAuth) | `protocol`, `status`, `code` |
182
+ | `SabreOrchestrationError` | Error de un servicio orquestado dentro de un `200` (`SP.*`) | `code` |
183
+ | `SabreBusinessError` | Error del catálogo propio de un servicio (p. ej. `No Availability`, una reserva que no se creó) | `service`, `code`, `warnings`, `errors` |
184
+ | `SabreWarning` | Aviso no fatal (p. ej. `category: WARNING` en Create Booking) | `service`, `code` |
185
+
186
+ Todos tienen `retryable`, que indica si la política de reintentos puede volver a intentar.
187
+
188
+ ```ts
189
+ import { SabreBusinessError, SabreGatewayError } from 'sabre-client';
190
+
191
+ try {
192
+ await sabre.booking.create(request);
193
+ } catch (error) {
194
+ if (error instanceof SabreBusinessError) {
195
+ console.error(error.service, error.code, error.message);
196
+ } else if (error instanceof SabreGatewayError) {
197
+ console.error(error.status, error.code);
198
+ }
199
+ throw error;
200
+ }
201
+ ```
202
+
203
+ > [!WARNING]
204
+ > `SabreError.raw` guarda el fragmento original de la respuesta, sin tocar, para debugging.
205
+ > **Puede contener credenciales o datos de pasajeros.** No es enumerable, así que no aparece en
206
+ > `JSON.stringify` ni en un spread, pero sigue accesible como `error.raw`. Nunca lo mandes a un
207
+ > logger, a un tracker de errores ni a un servicio de monitoreo sin sanitizarlo antes.
208
+
209
+ ## Reintentos
210
+
211
+ Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
212
+ (ambas son `POST` de solo lectura). Create Booking nunca se reintenta. Los reintentos usan
213
+ backoff exponencial con full jitter y un piso de 500 ms, ante `429`, `500`, `503`, `504`,
214
+ `SP.PROVIDER_ERROR` y los errores temporales del catálogo de Bargain Finder Max.
215
+
216
+ ## Reutilizar el token
217
+
218
+ Por defecto el token vive en memoria, uno por instancia de `Sabre`. Para compartirlo entre
219
+ procesos (scripts, funciones serverless), pasa un `tokenStore`. El SDK no trae ninguna
220
+ implementación:
221
+
222
+ ```ts
223
+ import type { SabreTokenStore } from 'sabre-client';
224
+
225
+ const tokenStore: SabreTokenStore = {
226
+ async get(key) {
227
+ return JSON.parse((await kv.get(key)) ?? 'null') ?? undefined;
228
+ },
229
+ async set(key, token) {
230
+ await kv.put(key, JSON.stringify(token));
231
+ },
232
+ };
233
+ ```
234
+
235
+ `key` identifica el entorno, la versión de OAuth y un hash de la identidad de la credencial,
236
+ así que un mismo store sirve a varios clientes. No contiene secretos, pero el token guardado sí
237
+ lo es: protegerlo te toca a ti. Si `get` o `set` lanzan, el SDK sigue con su caché en memoria.
238
+
239
+ ## Logging
240
+
241
+ No hay logging salvo que pases un `logger`. Es compatible con `console` y `winston`; con `pino`,
242
+ invierte los argumentos:
243
+
244
+ ```ts
245
+ const sabre = new Sabre({
246
+ environment: 'sandbox',
247
+ credentials,
248
+ logger: {
249
+ debug: (m, c) => pino.debug(c, m),
250
+ info: (m, c) => pino.info(c, m),
251
+ warn: (m, c) => pino.warn(c, m),
252
+ error: (m, c) => pino.error(c, m),
253
+ },
254
+ });
255
+ ```
256
+
257
+ El SDK registra eventos estructurados (`sabre.request.start`, `sabre.request.end`,
258
+ `sabre.request.retry`, `sabre.auth.token.refresh`, `sabre.auth.token.store`) y nunca los bodies
259
+ crudos de request o response. Los secretos, el header `Authorization` y los datos de pasajeros
260
+ se redactan siempre antes de llegar a tu logger.
261
+
262
+ ## Changelog
263
+
264
+ Ver [CHANGELOG.md](./CHANGELOG.md).
package/README.md CHANGED
@@ -1,148 +1,264 @@
1
- # Sabre Node.js SDK
1
+ # sabre-client
2
2
 
3
- Node.js library for the Sabre API (REST).
3
+ TypeScript SDK for the Sabre APIs.
4
4
 
5
- ## Install
5
+ [Leer en español](./README.es.md)
6
6
 
7
- ```bash
8
- npm install sabre-client
9
- ```
7
+ > **`0.3.0` is a complete rewrite, not an incremental update to `0.2.4`.**
8
+ > It is built on a different architecture (a `Sabre` REST client with a managed
9
+ > `AuthManager`, a typed error hierarchy, injectable logging with mandatory redaction, and
10
+ > runtime-agnostic Web Standard APIs). Code written against `0.2.4` will not work unchanged.
11
+ > Older versions stay published as they are.
10
12
 
11
- ## Setup
13
+ ## Status
12
14
 
13
- First, you need to get an username, password and pcc which is available in the [DEV STUDIO - Applications](https://developer.sabre.com/my-account/applications).
15
+ `0.3.0` is **not released yet**. It will be released once both Tier 1 and Tier 2 of the
16
+ Air Booking flow are done. Until then, `npm install sabre-client` installs `0.2.4`.
14
17
 
15
- ```js
16
- import { Sabre } from 'sabre-client';
18
+ Prereleases of the rewrite are published under the `next` tag:
17
19
 
18
- const sabre = new Sabre({
19
- username: '773400',
20
- password: 'PASSWORD_GOES_HERE',
21
- organization: '7TZA', // pcc
22
- });
20
+ ```sh
21
+ npm install sabre-client@next
23
22
  ```
24
23
 
25
- ## Documentation
26
-
27
- See http://developer.sabre.com
24
+ The API may still change before `0.3.0`.
28
25
 
29
- ## Usage
26
+ Tier 1 covers the core Air Booking flow over REST:
30
27
 
31
- Send the request for acquiring an authentication ATK session token.
28
+ | Step | Service | Method |
29
+ | - | - | - |
30
+ | Authentication | OAuth Token Create v3 / v2 | automatic |
31
+ | Shopping | Bargain Finder Max v5 | `sabre.air.bargainFinderMax` |
32
+ | Revalidation | Revalidate Itinerary v5 | `sabre.air.revalidateItinerary` |
33
+ | Booking | Create Booking (Booking Management API v1, ATPCO by `flightDetails`) | `sabre.booking.create` |
34
+ | Booking management | Get Booking and Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
32
35
 
33
- ```js
34
- await sabre.authentication.OAuthTokenV2();
35
- ```
36
+ Planned: Get Seats and Get Ancillaries (Tier 2), and a `SabreLegacy` client for SOAP services.
36
37
 
37
- ### Get booking v1.30
38
+ ## Requirements
38
39
 
39
- Displays comprehensive booking details irrespective of source, optionally narrowing down the response to selected elements.
40
+ - Node.js `>=22.12`, or a server-side edge runtime with `fetch` and `crypto.subtle`
41
+ (Cloudflare Workers, Vercel Edge Functions, Deno Deploy). The package uses only Web
42
+ Standard APIs.
43
+ - ESM only.
44
+ - **Never run it in a browser.** Your Sabre credentials would be exposed to anyone with
45
+ devtools.
40
46
 
41
- ```js
42
- await sabre.booking.get({
43
- confirmationId: 'GLEBNY',
44
- returnOnly: [],
45
- });
46
- ```
47
+ ## Configuration
47
48
 
48
- ### Create booking
49
+ The SDK never reads environment variables. Pass the configuration explicitly:
49
50
 
50
- Creates an air booking (NDC/ATPCO/LCC).
51
+ ```ts
52
+ import { Sabre } from 'sabre-client';
51
53
 
52
- ```js
53
- await sabre.booking.create(options);
54
+ const sabre = new Sabre({
55
+ environment: 'sandbox', // 'sandbox' (CERT) or 'production'
56
+ credentials: {
57
+ clientId: process.env.SABRE_CLIENT_ID!,
58
+ clientSecret: process.env.SABRE_CLIENT_SECRET!,
59
+ username: process.env.SABRE_EPR_USERNAME!, // EPR user, e.g. `<user>-<PCC>-AA`
60
+ password: process.env.SABRE_EPR_PASSWORD!,
61
+ },
62
+ });
54
63
  ```
55
64
 
56
- ### Modify Booking
57
-
58
- Creates a list of booking items that need to be modified.
65
+ | Option | Default | Description |
66
+ | - | - | - |
67
+ | `environment` | required | `'sandbox'` → `api.cert.platform.sabre.com`, `'production'` → `api.platform.sabre.com` |
68
+ | `authVersion` | `'v3'` | OAuth Token Create version. `'v3'` needs `clientId`, `clientSecret`, `username` and `password`; `'v2'` only needs the EPR `username` (`V1:<user>:<PCC>:AA`) and `password` |
69
+ | `credentials` | required | See above |
70
+ | `logger` | none | Any object with `debug`, `info`, `warn` and `error` (see [Logging](#logging)) |
71
+ | `retry` | `{ maxRetries: 3, baseDelayMs: 500 }` | Client-wide retry policy; every request can override it |
72
+ | `fetch` | global `fetch` | Custom `fetch` (proxies, tests, other runtimes) |
73
+ | `tokenStore` | in-memory only | Shares the token between processes (see [Token reuse](#token-reuse)) |
74
+
75
+ ### Authentication
76
+
77
+ You never request a token yourself. The client gets one on the first call, caches it, renews
78
+ it about an hour before it expires, and shares a single in-flight request between concurrent
79
+ calls. If Sabre rejects a token with `401`, the client gets a new one and retries that request
80
+ once.
81
+
82
+ ## Booking a flight
83
+
84
+ The full Tier 1 flow: search, revalidate the chosen itinerary, and book it.
85
+
86
+ ```ts
87
+ import { Sabre, buildRevalidateItineraryRequest, buildBookingFlightDetails } from 'sabre-client';
88
+
89
+ const sabre = new Sabre({ environment: 'sandbox', credentials });
90
+
91
+ // 1. Search with Bargain Finder Max. The request is the body Sabre documents.
92
+ const search = {
93
+ OTA_AirLowFareSearchRQ: {
94
+ Version: '5',
95
+ POS: { Source: [{ PseudoCityCode: 'XXXX', RequestorID: { Type: '1', ID: '1', CompanyName: { Code: 'TN' } } }] },
96
+ OriginDestinationInformation: [
97
+ { DepartureDateTime: '2026-12-01T10:00:00', OriginLocation: { LocationCode: 'DFW' }, DestinationLocation: { LocationCode: 'LAX' } },
98
+ ],
99
+ TravelerInfoSummary: { AirTravelerAvail: [{ PassengerTypeQuantity: [{ Code: 'ADT', Quantity: 1 }] }] },
100
+ TPA_Extensions: { IntelliSellTransaction: { RequestType: { Name: '50ITINS' } } },
101
+ },
102
+ };
103
+ const found = await sabre.air.bargainFinderMax(search);
104
+
105
+ // Responses come with references already resolved: every itinerary has its legs,
106
+ // schedules and fares inline, no `ref` lookups needed.
107
+ const group = found.itineraryGroups[0]!;
108
+ const itinerary = group.itineraries[0]!;
109
+
110
+ // 2. Revalidate the chosen itinerary (availability and price, no inventory held).
111
+ const revalidated = await sabre.air.revalidateItinerary(
112
+ buildRevalidateItineraryRequest({ search, group, itinerary }),
113
+ );
114
+ const confirmedGroup = revalidated.itineraryGroups[0]!;
115
+ const confirmed = confirmedGroup.itineraries[0]!;
116
+
117
+ // 3. Book it through the ATPCO path (`flightDetails`).
118
+ const booking = await sabre.booking.create({
119
+ flightDetails: buildBookingFlightDetails({ group: confirmedGroup, itinerary: confirmed }),
120
+ travelers: [{ givenName: 'JANE', surname: 'DOE', passengerCode: 'ADT' }],
121
+ contactInfo: { emails: ['jane@example.com'], phones: ['5550000000'] },
122
+ });
59
123
 
60
- ```js
61
- await sabre.booking.modify(options);
124
+ console.log(booking.confirmationId); // PNR locator
62
125
  ```
63
126
 
64
- ### Cancel Booking
127
+ `buildBookingFlightDetails` maps the revalidated itinerary to the `flightDetails` block of
128
+ Create Booking. That mapping is not documented by Sabre and has not been checked against a
129
+ real sandbox booking yet, so review the request before you rely on it.
65
130
 
66
- Cancels a booking or specified booking items, optionally voiding or refunding related flight tickets.
131
+ `sabre.booking.create` is **never retried automatically**, because it is not idempotent. If
132
+ Sabre creates the booking with warnings or non-fatal errors, the call does not throw. Check
133
+ `booking.warnings` and `booking.errors`.
67
134
 
68
- ```js
69
- await sabre.booking.cancel(options);
70
- ```
71
-
72
- ### Void Flight Tickets
135
+ `sabre.booking.get` reads a booking by `confirmationId`. Unlike Create Booking, the response
136
+ has the booking fields at the top level (`bookingId`, `flights`, `travelers`, …), as Sabre
137
+ defines it. Use `returnOnly` to ask only for the sections you need, which also makes the call
138
+ faster:
73
139
 
74
- Voids tickets with ticket numbers listed in the request, optionally including nonelectronic tickets (paper).
75
-
76
- ```js
77
- await sabre.flightTickets.void(options);
140
+ ```ts
141
+ const pnr = await sabre.booking.get({
142
+ confirmationId: booking.confirmationId,
143
+ returnOnly: ['FLIGHTS', 'TRAVELERS'],
144
+ });
78
145
  ```
79
146
 
80
- ### Refund Flight Tickets
147
+ It throws `SabreBusinessError` only when there is no booking (for example
148
+ `BOOKING_NOT_FOUND`). If the booking exists, partial problems come back in `pnr.warnings`
149
+ and `pnr.errors`; an unticketed booking usually carries a `RESOURCE_UNAVAILABLE` warning
150
+ because it has no fare rules yet. It is a read, so transient failures are retried.
81
151
 
82
- Processes tickets by providing a request containing a list of ticket numbers which should be refunded. EMDs are currently not supported.
152
+ `sabre.booking.cancel` cancels the whole booking (`cancelAll: true`) or specific items by the
153
+ `itemId` that `get` returns:
83
154
 
84
- ```js
85
- await sabre.flightTickets.refund(options);
155
+ ```ts
156
+ await sabre.booking.cancel({ confirmationId: booking.confirmationId, cancelAll: true });
157
+ await sabre.booking.cancel({ confirmationId: pnr.bookingId, flights: [{ itemId: '2' }] });
86
158
  ```
87
159
 
88
- ### Check Flight Tickets
160
+ Like `create`, it is **never retried automatically**. Combinations that Sabre always rejects
161
+ (`cancelAll` together with item lists, no `cancelAll` and no list, `offerItemId` together with
162
+ `flightTicketOperation`) throw a `TypeError` before any request is sent. If Sabre answers
163
+ `NO_ITEMS_CANCELLED`, the call throws `SabreBusinessError` even though Sabre sends it as a
164
+ warning, because nothing was cancelled; the errors that explain why are in `error.errors`. An
165
+ `END_TRANSACTION_PROBLEM` warning means the confirmation is still pending: check the result
166
+ with `sabre.booking.get`.
89
167
 
90
- Checks tickets with ticket numbers listed in the request for void, refund and exchange conditions. EMDs are currently not supported.
168
+ Every request accepts an `AbortSignal`. Shopping calls also accept a per-request retry policy:
91
169
 
92
- ```js
93
- await sabre.flightTickets.check(options);
170
+ ```ts
171
+ await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
94
172
  ```
95
173
 
96
- ### Fulfill Flight Tickets
174
+ ## Error handling
97
175
 
98
- Processes the issuance of tickets and various electronic miscellaneous documents (EMDs). EMDs are currently not supported.
176
+ Every error thrown by the SDK extends `SabreError`:
99
177
 
100
- ```js
101
- await sabre.flightTickets.fulfill(options);
102
- ```
178
+ | Class | When | Extra fields |
179
+ | - | - | - |
180
+ | `SabreGatewayError` | HTTP or gateway failure (`ERR.2SG.*`, `401`, `429`, `5xx`, OAuth errors) | `protocol`, `status`, `code` |
181
+ | `SabreOrchestrationError` | Orchestrated service error inside a `200` (`SP.*`) | `code` |
182
+ | `SabreBusinessError` | Error from a service's own catalog (e.g. `No Availability`, a booking that was not created) | `service`, `code`, `warnings`, `errors` |
183
+ | `SabreWarning` | Non-fatal warning (e.g. `category: WARNING` in Create Booking) | `service`, `code` |
103
184
 
104
- ### Bargain Finder Max (Search)
185
+ All of them have `retryable`, which says whether the retry policy may try again.
105
186
 
106
- Searches for the lowest available priced itineraries for a given origin and destination.
187
+ ```ts
188
+ import { SabreBusinessError, SabreGatewayError } from 'sabre-client';
107
189
 
108
- ```js
109
- await sabre.search.shop(options);
190
+ try {
191
+ await sabre.booking.create(request);
192
+ } catch (error) {
193
+ if (error instanceof SabreBusinessError) {
194
+ console.error(error.service, error.code, error.message);
195
+ } else if (error instanceof SabreGatewayError) {
196
+ console.error(error.status, error.code);
197
+ }
198
+ throw error;
199
+ }
110
200
  ```
111
201
 
112
- ### Revalidate Itinerary (Search)
202
+ > [!WARNING]
203
+ > `SabreError.raw` holds the original response fragment, untouched, for debugging. It **can
204
+ > contain credentials or passenger data**. It is not enumerable, so it does not show up in
205
+ > `JSON.stringify` or object spreads, but you can still read `error.raw`. Never send it to a
206
+ > logger, error tracker or monitoring service without sanitizing it first.
113
207
 
114
- The Revalidate Itinerary API allows you to re-query the availability and price of a specific itinerary option without having to book it.
208
+ ## Retries
115
209
 
116
- ```js
117
- await sabre.search.revalidate(options);
118
- ```
210
+ Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
211
+ are read-only `POST`s). Create Booking is never retried. Retries use exponential backoff with
212
+ full jitter and a 500 ms floor, on `429`, `500`, `503`, `504`, `SP.PROVIDER_ERROR` and the
213
+ temporary errors in the Bargain Finder Max catalog.
119
214
 
120
- ### Issue Ticket (Ticketing)
215
+ ## Token reuse
121
216
 
122
- Issues an airline ticket from an existing PNR (booking).
217
+ By default the token lives in memory, one per `Sabre` instance. To share it between
218
+ processes (scripts, serverless functions), pass a `tokenStore`. The SDK does not ship any
219
+ implementation:
123
220
 
124
- ```js
125
- await sabre.ticketing.issue(options);
126
- ```
221
+ ```ts
222
+ import type { SabreTokenStore } from 'sabre-client';
127
223
 
128
- ## Development
224
+ const tokenStore: SabreTokenStore = {
225
+ async get(key) {
226
+ return JSON.parse((await kv.get(key)) ?? 'null') ?? undefined;
227
+ },
228
+ async set(key, token) {
229
+ await kv.put(key, JSON.stringify(token));
230
+ },
231
+ };
232
+ ```
129
233
 
130
- #### Install dependencies
234
+ `key` identifies the environment, the OAuth version and a hash of the credential identity,
235
+ so one store can serve several clients. It contains no secrets, but the stored token is one:
236
+ protecting it is up to you. If `get` or `set` throws, the SDK falls back to its in-memory
237
+ cache.
131
238
 
132
- ```sh
133
- npm install
134
- ```
239
+ ## Logging
135
240
 
136
- #### Build and run packages
241
+ Logging is off unless you pass a `logger`. It is compatible with `console` and `winston`;
242
+ for `pino`, swap the arguments:
137
243
 
138
- ```sh
139
- npm dev
244
+ ```ts
245
+ const sabre = new Sabre({
246
+ environment: 'sandbox',
247
+ credentials,
248
+ logger: {
249
+ debug: (m, c) => pino.debug(c, m),
250
+ info: (m, c) => pino.info(c, m),
251
+ warn: (m, c) => pino.warn(c, m),
252
+ error: (m, c) => pino.error(c, m),
253
+ },
254
+ });
140
255
  ```
141
256
 
142
- ## Authors
143
-
144
- - Carlos Ivan Soto ([carlosivansotop](https://www.linkedin.com/in/carlosivansotop/))
257
+ The SDK logs structured events (`sabre.request.start`, `sabre.request.end`,
258
+ `sabre.request.retry`, `sabre.auth.token.refresh`, `sabre.auth.token.store`), never raw
259
+ request or response bodies. Secrets, the `Authorization` header and passenger data are always
260
+ redacted before they reach your logger.
145
261
 
146
- ## License
262
+ ## Changelog
147
263
 
148
- MIT License
264
+ See [CHANGELOG.md](./CHANGELOG.md).