sabre-client 0.2.4 → 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/dist/index.js CHANGED
@@ -1,370 +1,1268 @@
1
- // src/air/search.ts
2
- var Search = class {
3
- /**
4
- * Bargain Finder Max v5
5
- *
6
- * Bargain Finder Max (BFM),
7
- * nuestro producto de búsqueda de tarifas bajas líder en su clase,
8
- * se utiliza para buscar los itinerarios con los precios más bajos disponibles según una fecha específica.
9
- *
10
- * Revalidate Itinerary v5
11
- *
12
- * La API Revalidate Itinerary permite volver a consultar la disponibilidad
13
- * y el precio de una opción de itinerario específica sin tener que reservarla.
14
- * La solución revalida si la opción de itinerario es válida para la compra.
15
- * Revalidate Itinerary no admite contenido NDC.
16
- *
17
- * @param sabre Sabre Client Reference
18
- */
19
- constructor(sabre) {
20
- this.sabre = sabre;
1
+ // src/errors/sabre-error.ts
2
+ var SabreError = class extends Error {
3
+ name = "SabreError";
4
+ retryable;
5
+ constructor(message, options) {
6
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
7
+ this.retryable = options.retryable;
8
+ Object.defineProperty(this, "raw", {
9
+ value: options.raw,
10
+ enumerable: false,
11
+ writable: false,
12
+ configurable: false
13
+ });
21
14
  }
22
- /**
23
- * Crear solicitud de Buscador de ofertas Max.
24
- *
25
- * payload - El mensaje de solicitud de búsqueda de tarifa baja solicita opciones
26
- * de itinerario para vuelos entre pares de ciudades específicos, en fechas específicas,
27
- * para un número y tipo de pasajeros específicos.
28
- * La información opcional de la solicitud puede incluir:
29
- * hora/ventana horaria, ciudades de conexión
30
- * y preferencias del cliente (aerolíneas, cabina, tipo de vuelo, etc.).
31
- * La solicitud de búsqueda de tarifa baja contiene información similar a
32
- * la de una búsqueda de tarifa baja en el CRS o GDS de una aerolínea.
33
- * @param payload BargainFinderMaxOptions
34
- * @returns {Promise<BargainFinderMaxResponseSuccess>}
35
- */
36
- async shop(payload) {
37
- const path = "/v5/offers/shop";
38
- return this.sabre.post(path, payload);
15
+ toJSON() {
16
+ const fields = Object.fromEntries(Object.entries(this));
17
+ return { ...fields, name: this.name, message: this.message };
39
18
  }
40
- /**
41
- * Crear solicitud de revalidación de itinerario.
42
- *
43
- * payload - El mensaje de solicitud de búsqueda de tarifa baja solicita opciones
44
- * de itinerario para vuelos entre pares de ciudades específicos, en fechas específicas,
45
- * para un número y tipo de pasajeros específicos.
46
- * La información opcional de la solicitud puede incluir:
47
- * hora/ventana horaria, ciudades de conexión
48
- * y preferencias del cliente (aerolíneas, cabina, tipo de vuelo, etc.).
49
- * La solicitud de búsqueda de tarifa baja contiene información similar a
50
- * la de una búsqueda de tarifa baja en el CRS o GDS de una aerolínea.
51
- * @param payload RevalidateItineraryOptions
52
- * @returns {Promise<RevalidateItineraryResponseSuccess>}
53
- */
54
- async revalidate(payload) {
55
- const path = "/v5/shop/flights/revalidate";
56
- return this.sabre.post(path, payload);
19
+ };
20
+ var SabreGatewayError = class extends SabreError {
21
+ name = "SabreGatewayError";
22
+ protocol;
23
+ status;
24
+ code;
25
+ constructor(message, options) {
26
+ super(message, options);
27
+ this.protocol = options.protocol;
28
+ this.status = options.status;
29
+ this.code = options.code;
30
+ }
31
+ };
32
+ var SabreOrchestrationError = class extends SabreError {
33
+ name = "SabreOrchestrationError";
34
+ code;
35
+ constructor(message, options) {
36
+ super(message, { ...options, retryable: options.code === "SP.PROVIDER_ERROR" });
37
+ this.code = options.code;
38
+ }
39
+ };
40
+ var SabreBusinessError = class extends SabreError {
41
+ name = "SabreBusinessError";
42
+ service;
43
+ code;
44
+ warnings;
45
+ errors;
46
+ constructor(message, options) {
47
+ super(message, { ...options, retryable: options.retryable ?? false });
48
+ this.service = options.service;
49
+ this.code = options.code;
50
+ this.warnings = options.warnings ?? [];
51
+ this.errors = options.errors ?? [];
52
+ }
53
+ };
54
+ var SabreWarning = class extends SabreError {
55
+ name = "SabreWarning";
56
+ service;
57
+ code;
58
+ constructor(message, options) {
59
+ super(message, { ...options, retryable: false });
60
+ this.service = options.service;
61
+ this.code = options.code;
57
62
  }
58
63
  };
59
64
 
60
- // src/air/ticketing.ts
61
- var Ticketing = class {
62
- /**
63
- * Enhanced Air Ticket v1.3.0
64
- *
65
- * La API de boletos aéreos mejorados se utiliza para emitir uno o varios boletos aéreos
66
- * y/o EMD (documentos electrónicos varios) dentro de una sola llamada API.
67
- *
68
- * @param sabre Sabre Client Reference
69
- */
70
- constructor(sabre) {
71
- this.sabre = sabre;
65
+ // src/air/grouped-itinerary.ts
66
+ function denormalizeGroupedItineraryResponse(raw, service) {
67
+ const resolver = new Resolver(raw, service);
68
+ return {
69
+ version: raw.version,
70
+ messages: raw.messages ?? [],
71
+ ...raw.statistics === void 0 ? {} : { statistics: raw.statistics },
72
+ itineraryGroups: (raw.itineraryGroups ?? []).map((group) => ({
73
+ groupDescription: group.groupDescription,
74
+ itineraries: group.itineraries.map((itinerary) => resolver.itinerary(itinerary))
75
+ }))
76
+ };
77
+ }
78
+ var Resolver = class {
79
+ #service;
80
+ #schedules;
81
+ #legs;
82
+ #taxes;
83
+ #taxSummaries;
84
+ #obFees;
85
+ #fareComponents;
86
+ #validatingCarriers;
87
+ #baggageAllowances;
88
+ constructor(raw, service) {
89
+ this.#service = service;
90
+ this.#schedules = byId(raw.scheduleDescs);
91
+ this.#legs = byId(raw.legDescs);
92
+ this.#taxes = byId(raw.taxDescs);
93
+ this.#taxSummaries = byId(raw.taxSummaryDescs);
94
+ this.#obFees = byId(raw.obFeeDescs);
95
+ this.#fareComponents = byId(raw.fareComponentDescs);
96
+ this.#validatingCarriers = byId(raw.validatingCarrierDescs);
97
+ this.#baggageAllowances = byId(raw.baggageAllowanceDescs);
72
98
  }
73
- /**
74
- * La API de boletos aéreos mejorados (AirTicketRQ)
75
- * se utiliza para emitir boletos aéreos y documentos electrónicos varios (EMD) en una sola solicitud.
76
- *
77
- * payload - Solicitud de API de boletos aéreos orquestada.
78
- * @param payload EnhancedAirTicketOptions
79
- * @returns {Promise<EnhancedAirTicketResponseSuccess>}
80
- */
81
- async issue(payload) {
82
- const path = "/v1.3.0/air/ticket";
83
- return this.sabre.post(path, payload);
99
+ itinerary(itinerary) {
100
+ return {
101
+ ...itinerary,
102
+ legs: itinerary.legs.map((ref) => this.#leg(ref)),
103
+ pricingInformation: itinerary.pricingInformation.map((pricing) => ({
104
+ ...pricing,
105
+ fare: this.#fare(pricing.fare)
106
+ }))
107
+ };
108
+ }
109
+ #leg(ref) {
110
+ const leg = this.#merge(this.#legs, "legDescs", ref);
111
+ return { ...leg, schedules: leg.schedules.map((schedule) => this.#merge(this.#schedules, "scheduleDescs", schedule)) };
112
+ }
113
+ #fare(fare) {
114
+ const { validatingCarriers, ...rest } = fare;
115
+ return {
116
+ ...rest,
117
+ passengerInfoList: fare.passengerInfoList.map(({ passengerInfo, ...entry }) => ({
118
+ ...entry,
119
+ passengerInfo: this.#passengerInfo(passengerInfo)
120
+ })),
121
+ ...validatingCarriers === void 0 ? {} : {
122
+ validatingCarriers: validatingCarriers.map(
123
+ (ref) => this.#merge(this.#validatingCarriers, "validatingCarrierDescs", ref)
124
+ )
125
+ }
126
+ };
127
+ }
128
+ #passengerInfo(info) {
129
+ const { fareComponents, taxes, taxSummaries, obFees, baggageInformation, ...rest } = info;
130
+ const result = { ...rest };
131
+ if (fareComponents !== void 0) result.fareComponents = fareComponents.map((ref) => this.#fareComponent(ref));
132
+ if (taxes !== void 0) result.taxes = taxes.map((ref) => this.#merge(this.#taxes, "taxDescs", ref));
133
+ if (taxSummaries !== void 0) {
134
+ result.taxSummaries = taxSummaries.map((ref) => this.#merge(this.#taxSummaries, "taxSummaryDescs", ref));
135
+ }
136
+ if (obFees !== void 0) result.obFees = obFees.map((ref) => this.#merge(this.#obFees, "obFeeDescs", ref));
137
+ if (baggageInformation !== void 0) {
138
+ result.baggageInformation = baggageInformation.map(({ allowance, ...bag }) => ({
139
+ ...bag,
140
+ ...allowance === void 0 ? {} : { allowance: this.#merge(this.#baggageAllowances, "baggageAllowanceDescs", allowance) }
141
+ }));
142
+ }
143
+ return result;
144
+ }
145
+ #fareComponent(ref) {
146
+ const desc = this.#lookup(this.#fareComponents, "fareComponentDescs", ref.ref);
147
+ const { ref: _ref, segments: siteSegments, ...site } = ref;
148
+ const { segments: descSegments, ...descRest } = desc;
149
+ if (descSegments !== void 0 && siteSegments !== void 0 && descSegments.length !== siteSegments.length) {
150
+ throw this.#malformed(
151
+ `fareComponents con ${siteSegments.length} segmentos y fareComponentDescs ${ref.ref} con ${descSegments.length}`
152
+ );
153
+ }
154
+ const segments = mergeSegments(descSegments, siteSegments);
155
+ return { ...descRest, ...site, ...segments === void 0 ? {} : { segments } };
156
+ }
157
+ /** `{ ref, ...resto }` → `{ ...descriptor, ...resto }`. */
158
+ #merge(index, kind, ref) {
159
+ const { ref: id, ...site } = ref;
160
+ return { ...this.#lookup(index, kind, id), ...site };
161
+ }
162
+ #lookup(index, kind, id) {
163
+ const desc = index.get(id);
164
+ if (desc === void 0) throw this.#malformed(`ref ${id} sin descriptor en ${kind}`);
165
+ return desc;
166
+ }
167
+ #malformed(detail) {
168
+ return new SabreError(`${this.#service}: respuesta malformada, ${detail}`, { retryable: false });
84
169
  }
85
170
  };
171
+ function byId(descs) {
172
+ return new Map((descs ?? []).map((desc) => [desc.id, desc]));
173
+ }
174
+ function mergeSegments(desc, site) {
175
+ if (desc === void 0 && site === void 0) return void 0;
176
+ const length = desc?.length ?? site?.length ?? 0;
177
+ return Array.from({ length }, (_, i) => ({
178
+ segment: { ...desc?.[i]?.segment, ...site?.[i]?.segment }
179
+ }));
180
+ }
86
181
 
87
- // src/authentication/authentication.ts
88
- var grantType = new URLSearchParams({ "grant_type": "client_credentials" }).toString();
89
- var Authentication = class {
90
- constructor(sabre) {
91
- this.sabre = sabre;
182
+ // src/air/itinerary-messages.ts
183
+ function classifyItineraryMessages(body, service, catalog) {
184
+ const response = isObject(body) ? body.groupedItineraryResponse : void 0;
185
+ if (!isObject(response)) {
186
+ return new SabreError(`${service}: respuesta malformada, falta groupedItineraryResponse`, {
187
+ retryable: false,
188
+ raw: body
189
+ });
92
190
  }
93
- /**
94
- * OAuth Token Create REST API v2
95
- * See https://developer.sabre.com/docs/rest_apis/session_management/token_create_api/v2
96
- * @param payload El tipo de concesión utilizado para obtener el token. Sólo se admiten 'client_credentials'.
97
- * @returns TokenResponse
98
- */
99
- async OAuthTokenV2(payload = grantType) {
100
- const data = await this.sabre.auth(
101
- "/v2/auth/token",
102
- payload
191
+ const messages = Array.isArray(response.messages) ? response.messages : [];
192
+ const error = messages.find((message) => message.severity?.toLowerCase() === "error");
193
+ if (error === void 0) return void 0;
194
+ const match = findInCatalog(messages, catalog);
195
+ const text = match?.text ?? (typeof error.text === "string" ? error.text : "");
196
+ const known = match?.entry;
197
+ const code = known?.code ?? error.code ?? text;
198
+ return new SabreBusinessError(`${service}: ${text || code}`, {
199
+ service,
200
+ code,
201
+ retryable: known?.retryable ?? false,
202
+ raw: body
203
+ });
204
+ }
205
+ function findInCatalog(messages, catalog) {
206
+ for (const message of messages) {
207
+ if (typeof message.text !== "string") continue;
208
+ const text = message.text.toLowerCase();
209
+ const entry = catalog.find((candidate) => text.includes(candidate.code.toLowerCase()));
210
+ if (entry !== void 0) return { entry, text: message.text };
211
+ }
212
+ return void 0;
213
+ }
214
+ function isObject(value) {
215
+ return typeof value === "object" && value !== null;
216
+ }
217
+
218
+ // src/air/bargain-finder-max.ts
219
+ var BARGAIN_FINDER_MAX_SERVICE = "bargainFinderMax";
220
+ var PATH = "/v5/offers/shop";
221
+ var BARGAIN_FINDER_MAX_CATALOG = [
222
+ { code: "No Availability", retryable: false },
223
+ { code: "NO FLIGHT SCHEDULES FOR QUALIFIERS USED", retryable: false },
224
+ { code: "Connection reset by peer", retryable: true },
225
+ { code: "Transmission timeout", retryable: true },
226
+ { code: "Insufficient resources", retryable: true },
227
+ { code: "MAX NBR COMBINATIONS EXCEEDED", retryable: true }
228
+ ];
229
+ async function bargainFinderMax(transport, request, options = {}) {
230
+ if (typeof request !== "object" || request === null || typeof request.OTA_AirLowFareSearchRQ !== "object") {
231
+ throw new TypeError("sabre-client: bargainFinderMax espera { OTA_AirLowFareSearchRQ: { ... } }");
232
+ }
233
+ const body = await transport.request({
234
+ method: "POST",
235
+ path: PATH,
236
+ service: BARGAIN_FINDER_MAX_SERVICE,
237
+ // POST, pero de solo lectura: se puede reintentar sin efectos duplicados.
238
+ idempotent: true,
239
+ body: request,
240
+ classify: classifyBargainFinderMax,
241
+ ...options.retry === void 0 ? {} : { retry: options.retry },
242
+ ...options.signal === void 0 ? {} : { signal: options.signal }
243
+ });
244
+ return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, BARGAIN_FINDER_MAX_SERVICE);
245
+ }
246
+ function classifyBargainFinderMax(body) {
247
+ return classifyItineraryMessages(body, BARGAIN_FINDER_MAX_SERVICE, BARGAIN_FINDER_MAX_CATALOG);
248
+ }
249
+
250
+ // src/air/itinerary-flights.ts
251
+ function itineraryFlights(group, itinerary) {
252
+ const { legDescriptions } = group.groupDescription;
253
+ if (legDescriptions.length !== itinerary.legs.length) {
254
+ throw new TypeError(
255
+ `sabre-client: el itinerario tiene ${itinerary.legs.length} tramos y el grupo ${legDescriptions.length}`
103
256
  );
104
- this.sabre.setAuthorization(data.access_token);
105
- return data;
106
257
  }
107
- };
258
+ const classes = bookingCodes(itinerary);
259
+ const flightCount = itinerary.legs.reduce((n, leg) => n + leg.schedules.length, 0);
260
+ if (classes.length !== flightCount) {
261
+ throw new TypeError(
262
+ `sabre-client: el itinerario tiene ${flightCount} vuelos y ${classes.length} clases de servicio en fareComponents`
263
+ );
264
+ }
265
+ let next = 0;
266
+ return itinerary.legs.flatMap((leg, legIndex) => {
267
+ const legDate = legDescriptions[legIndex].departureDate;
268
+ return leg.schedules.map((schedule) => {
269
+ const departureDate = addDays(legDate, schedule.departureDateAdjustment ?? 0);
270
+ return {
271
+ legIndex,
272
+ schedule,
273
+ departureDate,
274
+ arrivalDate: addDays(departureDate, schedule.arrival.dateAdjustment ?? 0),
275
+ bookingCode: classes[next++]
276
+ };
277
+ });
278
+ });
279
+ }
280
+ function bookingCodes(itinerary) {
281
+ const passenger = itinerary.pricingInformation[0]?.fare.passengerInfoList[0]?.passengerInfo;
282
+ return (passenger?.fareComponents ?? []).flatMap(
283
+ (component) => (component.segments ?? []).map(({ segment }) => {
284
+ if (segment.bookingCode === void 0) {
285
+ throw new TypeError("sabre-client: un segmento de fareComponents no trae bookingCode");
286
+ }
287
+ return segment.bookingCode;
288
+ })
289
+ );
290
+ }
291
+ function addDays(date, days) {
292
+ if (days === 0) return date;
293
+ const [year, month, day] = date.split("-").map(Number);
294
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
295
+ }
108
296
 
109
- // src/booking-management/booking.ts
110
- var Booking = class {
111
- /**
112
- * Booking
113
- *
114
- * La API de reservas está diseñada para proporcionar una vista normalizada de una reserva de Sabre combinando el PNR de Sabre y el pedido de Sabre.
115
- *
116
- * @param sabre Sabre Client Reference
117
- */
118
- constructor(sabre) {
119
- this.sabre = sabre;
297
+ // src/air/revalidate-itinerary.ts
298
+ var REVALIDATE_ITINERARY_SERVICE = "revalidateItinerary";
299
+ var PATH2 = "/v5/shop/flights/revalidate";
300
+ async function revalidateItinerary(transport, request, options = {}) {
301
+ if (typeof request !== "object" || request === null || typeof request.OTA_AirLowFareSearchRQ !== "object") {
302
+ throw new TypeError("sabre-client: revalidateItinerary espera { OTA_AirLowFareSearchRQ: { ... } }");
120
303
  }
121
- /**
122
- * Crea una solicitud que contiene un "Id. de confirmación" y un "apellido" del viajero para obtener información de la reserva.
123
- *
124
- * payload - Contiene elementos tanto obligatorios como opcionales para realizar una solicitud de reserva.
125
- * @param payload GetBookingOptions
126
- * @returns {Promise<GetBookingResponseSuccess>}
127
- */
128
- get(payload) {
129
- const path = "/v1/trip/orders/getBooking";
130
- return this.sabre.post(path, payload);
304
+ const body = await transport.request({
305
+ method: "POST",
306
+ path: PATH2,
307
+ service: REVALIDATE_ITINERARY_SERVICE,
308
+ // POST, pero de solo lectura: no sostiene inventario, se puede reintentar.
309
+ idempotent: true,
310
+ body: request,
311
+ classify: classifyRevalidateItinerary,
312
+ ...options.retry === void 0 ? {} : { retry: options.retry },
313
+ ...options.signal === void 0 ? {} : { signal: options.signal }
314
+ });
315
+ return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, REVALIDATE_ITINERARY_SERVICE);
316
+ }
317
+ var REVALIDATE_ITINERARY_CATALOG = [
318
+ { code: "NO COMBINABLE FARES FOR CLASS USED", retryable: false },
319
+ { code: "NO FARE FOR CLASS USED", retryable: false },
320
+ { code: "NO FLIGHT SCHEDULES FOR QUALIFIERS USED", retryable: false },
321
+ { code: "NO SUCH FLIGHT", retryable: false },
322
+ { code: "NO AVAILABILITY", retryable: false },
323
+ ...BARGAIN_FINDER_MAX_CATALOG.filter((entry) => entry.retryable)
324
+ ];
325
+ function classifyRevalidateItinerary(body) {
326
+ return classifyItineraryMessages(body, REVALIDATE_ITINERARY_SERVICE, REVALIDATE_ITINERARY_CATALOG);
327
+ }
328
+ function buildRevalidateItineraryRequest(source) {
329
+ const { search, group, itinerary } = source;
330
+ const flights = itineraryFlights(group, itinerary);
331
+ const originDestinations = itinerary.legs.map((_leg, i) => {
332
+ const legFlights = flights.filter((f) => f.legIndex === i).map(flight);
333
+ const first = legFlights[0];
334
+ const last = legFlights.at(-1);
335
+ return {
336
+ Fixed: false,
337
+ DepartureDateTime: first.DepartureDateTime,
338
+ OriginLocation: { LocationCode: first.OriginLocation.LocationCode },
339
+ DestinationLocation: { LocationCode: last.DestinationLocation.LocationCode },
340
+ TPA_Extensions: { Flight: legFlights }
341
+ };
342
+ });
343
+ const { Version, POS, TravelerInfoSummary, TPA_Extensions } = search.OTA_AirLowFareSearchRQ;
344
+ return {
345
+ OTA_AirLowFareSearchRQ: {
346
+ Version,
347
+ POS,
348
+ OriginDestinationInformation: originDestinations,
349
+ TravelPreferences: {
350
+ MaxStopsQuantity: Math.max(...itinerary.legs.map((leg) => leg.schedules.length - 1)),
351
+ TPA_Extensions: { VerificationItinCallLogic: { Value: "L" } }
352
+ },
353
+ TravelerInfoSummary,
354
+ ...TPA_Extensions === void 0 ? {} : { TPA_Extensions }
355
+ }
356
+ };
357
+ }
358
+ function flight({ schedule, departureDate, arrivalDate, bookingCode }) {
359
+ const { carrier } = schedule;
360
+ return {
361
+ Type: "A",
362
+ Number: carrier.marketingFlightNumber,
363
+ DepartureDateTime: `${departureDate}T${localTime(schedule.departure.time)}`,
364
+ ArrivalDateTime: `${arrivalDate}T${localTime(schedule.arrival.time)}`,
365
+ ClassOfService: bookingCode,
366
+ OriginLocation: { LocationCode: schedule.departure.airport },
367
+ DestinationLocation: { LocationCode: schedule.arrival.airport },
368
+ Airline: {
369
+ Marketing: carrier.marketing,
370
+ ...carrier.operating === void 0 ? {} : { Operating: carrier.operating }
371
+ }
372
+ };
373
+ }
374
+ function localTime(time) {
375
+ return time.slice(0, 8);
376
+ }
377
+
378
+ // src/air/air.ts
379
+ var Air = class {
380
+ #transport;
381
+ /** @internal Lo construye el cliente `Sabre`. */
382
+ constructor(transport) {
383
+ this.#transport = transport;
131
384
  }
132
385
  /**
133
- * Crea una reserva aérea (NDC/ATPCO/LCC).
134
- *
135
- * payload - Contiene elementos obligatorios y opcionales para realizar una solicitud de reserva.
136
- * @param payload CreateBookingOptions
137
- * @returns {Promise<CreateBookingResponseSuccess>}
386
+ * Bargain Finder Max v5: busca itinerarios de menor precio. Devuelve el
387
+ * `groupedItineraryResponse` con las referencias ya resueltas.
138
388
  */
139
- async create(payload) {
140
- const path = "/v1/trip/orders/createBooking";
141
- return this.sabre.post(path, payload);
389
+ bargainFinderMax(request, options) {
390
+ return bargainFinderMax(this.#transport, request, options);
142
391
  }
143
392
  /**
144
- * Crea una lista de elementos de reserva que deben cancelarse.
145
- *
146
- * payload - Contiene elementos obligatorios y opcionales para cancelar la totalidad o partes de una reserva.
147
- * @param payload CancelBookingOptions
148
- * @returns {Promise<CancelBookingResponseSuccess>}
393
+ * Revalidate Itinerary v5: revalida disponibilidad y precio de un itinerario ya elegido,
394
+ * sin sostener inventario. El request se arma con `buildRevalidateItineraryRequest`.
149
395
  */
150
- async cancel(payload) {
151
- const path = "/v1/trip/orders/cancelBooking";
152
- return this.sabre.post(path, payload);
396
+ revalidateItinerary(request, options) {
397
+ return revalidateItinerary(this.#transport, request, options);
153
398
  }
154
- /**
155
- * Crea una lista de elementos de reserva que deben modificarse.
156
- *
157
- * payload - Contiene elementos obligatorios y opcionales para modificar datos no relacionados con el itinerario en la reserva existente.
158
- * @param payload ModifyBookingOptions
159
- * @returns {Promise<ModifyBookingResponseSuccess>}
160
- */
161
- async modify(payload) {
162
- const path = "/v1/trip/orders/modifyBooking";
163
- return this.sabre.post(path, payload);
399
+ };
400
+
401
+ // src/logging/redact.ts
402
+ var REDACTED = "[REDACTED]";
403
+ var SENSITIVE_KEYS = new Set(
404
+ [
405
+ // Credenciales
406
+ "clientSecret",
407
+ "password",
408
+ "username",
409
+ "authorization",
410
+ "binarySecurityToken",
411
+ "accessToken",
412
+ "refreshToken",
413
+ "token",
414
+ // Datos de pasajero
415
+ "travelers",
416
+ "passengers"
417
+ ].map(normalizeKey)
418
+ );
419
+ function normalizeKey(key) {
420
+ return key.replace(/[-_\s]/g, "").toLowerCase();
421
+ }
422
+ function isSensitiveKey(key) {
423
+ return SENSITIVE_KEYS.has(normalizeKey(key));
424
+ }
425
+ function redact(value) {
426
+ return redactValue(value, /* @__PURE__ */ new WeakSet());
427
+ }
428
+ function redactValue(value, seen) {
429
+ if (value === null || typeof value !== "object") return value;
430
+ if (seen.has(value)) return "[Circular]";
431
+ seen.add(value);
432
+ if (Array.isArray(value)) return value.map((item) => redactValue(item, seen));
433
+ if (value instanceof Headers) return redactEntries(value.entries(), seen);
434
+ if (value instanceof Date) return value.toISOString();
435
+ if (value instanceof Error) {
436
+ const json = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : void 0;
437
+ return redactValue(json ?? { name: value.name, message: value.message }, seen);
438
+ }
439
+ return redactEntries(Object.entries(value), seen);
440
+ }
441
+ function redactEntries(entries, seen) {
442
+ const out = {};
443
+ for (const [key, item] of entries) {
444
+ out[key] = isSensitiveKey(key) ? REDACTED : redactValue(item, seen);
445
+ }
446
+ return out;
447
+ }
448
+
449
+ // src/logging/logger.ts
450
+ var EventLogger = class {
451
+ #logger;
452
+ constructor(logger) {
453
+ this.#logger = logger;
454
+ }
455
+ emit(level, event, context = {}) {
456
+ if (this.#logger === void 0) return;
457
+ try {
458
+ this.#logger[level](event, redact(context));
459
+ } catch {
460
+ }
164
461
  }
165
462
  };
166
463
 
167
- // src/booking-management/flight-tickets.ts
168
- var FlightTickets = class {
169
- /**
170
- * Flight Tickets
171
- *
172
- * Los endpoint de Flight Tickets son un conjunto de métodos diseñados para obtener detalles de los billetes y
173
- * realizar operaciones específicas (anulación/reembolso) sobre ellos.
174
- *
175
- * @param sabre Sabre Client Reference
176
- */
177
- constructor(sabre) {
178
- this.sabre = sabre;
464
+ // src/auth/token-request.ts
465
+ function buildTokenRequestV3(credentials) {
466
+ return {
467
+ path: "/v3/auth/token",
468
+ authorization: `Basic ${base64(`${credentials.clientId}:${credentials.clientSecret}`)}`,
469
+ body: new URLSearchParams({
470
+ grant_type: "password",
471
+ username: credentials.username,
472
+ password: credentials.password
473
+ })
474
+ };
475
+ }
476
+ function buildTokenRequestV2(credentials) {
477
+ return {
478
+ path: "/v2/auth/token",
479
+ authorization: `Basic ${base64(`${base64(credentials.username)}:${base64(credentials.password)}`)}`,
480
+ body: new URLSearchParams({ grant_type: "client_credentials" })
481
+ };
482
+ }
483
+ function buildTokenRequest(authVersion, credentials) {
484
+ return authVersion === "v3" ? buildTokenRequestV3(credentials) : buildTokenRequestV2(credentials);
485
+ }
486
+ function base64(text) {
487
+ let binary = "";
488
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
489
+ return btoa(binary);
490
+ }
491
+
492
+ // src/auth/token-store.ts
493
+ async function tokenStoreKey(environment, authVersion, credentials) {
494
+ const identity = authVersion === "v3" ? `${credentials.clientId}
495
+ ${credentials.username}` : credentials.username;
496
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity));
497
+ const hash = [...new Uint8Array(digest).slice(0, 16)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
498
+ return `sabre-client:${environment}:${authVersion}:${hash}`;
499
+ }
500
+ function isStoredToken(value) {
501
+ if (typeof value !== "object" || value === null) return false;
502
+ const { accessToken, obtainedAt, expiresAt } = value;
503
+ return typeof accessToken === "string" && accessToken !== "" && typeof obtainedAt === "number" && typeof expiresAt === "number" && Number.isFinite(obtainedAt) && expiresAt > obtainedAt;
504
+ }
505
+
506
+ // src/auth/auth-manager.ts
507
+ var TOKEN_SERVICE = "oauthTokenCreate";
508
+ var REFRESH_THRESHOLD_S = 3600;
509
+ var AuthManager = class {
510
+ #config;
511
+ #transport;
512
+ #now;
513
+ #log;
514
+ #token;
515
+ #inFlight;
516
+ /** El último token cacheado fue rechazado con 401 (el próximo refresco es reactivo). */
517
+ #invalidated = false;
518
+ /** Token rechazado con 401: si el store todavía lo tiene, no se vuelve a usar. */
519
+ #rejected;
520
+ #store;
521
+ #storeKey;
522
+ constructor(config, transport, options = {}) {
523
+ this.#config = config;
524
+ this.#transport = transport;
525
+ this.#now = options.now ?? Date.now;
526
+ this.#log = new EventLogger(config.logger);
527
+ this.#store = config.tokenStore;
179
528
  }
180
- /**
181
- * Procesa tickets proporcionando una solicitud que contiene una lista de números de ticket que se anularán.
182
- *
183
- * payload - Contiene elementos obligatorios y opcionales para anular los billetes.
184
- * @param payload VoidTicketsOptions
185
- * @returns {Promise<VoidTicketsResponseSuccess>}
186
- */
187
- async void(payload) {
188
- const path = "/v1/trip/orders/voidFlightTickets";
189
- return this.sabre.post(path, payload);
529
+ /** Token vigente; pide uno nuevo si no hay o si cruzó el umbral de refresco. */
530
+ async getToken() {
531
+ const token = this.#token;
532
+ if (token !== void 0 && !this.#needsRefresh(token)) return token.accessToken;
533
+ const reason = token !== void 0 ? "proactive" : this.#invalidated ? "reactive" : "initial";
534
+ return (await this.#refresh(reason)).accessToken;
190
535
  }
191
536
  /**
192
- * Procesa los boletos proporcionando una solicitud que contiene una lista de números de boletos que deben reembolsarse.
193
- * Actualmente, los EMD no son compatibles.
194
- *
195
- * payload - Contiene elementos obligatorios y opcionales para el reembolso de los billetes.
196
- * @param payload RefundTicketsOptions
197
- * @returns {Promise<RefundTicketsResponseSuccess>}
537
+ * Descarta `accessToken` tras un 401 de un servicio. Si el caché ya tiene otro token
538
+ * (otra request lo renovó), no hace nada: los 401 concurrentes comparten un refresco.
198
539
  */
199
- async refund(payload) {
200
- const path = "/v1/trip/orders/refundFlightTickets";
201
- return this.sabre.post(path, payload);
540
+ invalidate(accessToken) {
541
+ if (this.#token?.accessToken !== accessToken) return;
542
+ this.#token = void 0;
543
+ this.#invalidated = true;
544
+ this.#rejected = accessToken;
545
+ }
546
+ /** Adaptador de salida para el transporte REST: `Authorization: Bearer <token>`. */
547
+ bearerProvider() {
548
+ return {
549
+ get: async () => `Bearer ${await this.getToken()}`,
550
+ invalidate: (authorization) => this.invalidate(authorization.replace(/^Bearer /, ""))
551
+ };
552
+ }
553
+ #needsRefresh(token) {
554
+ const thresholdMs = Math.min(REFRESH_THRESHOLD_S, token.expiresIn / 2) * 1e3;
555
+ return this.#now() >= token.obtainedAt + token.expiresIn * 1e3 - thresholdMs;
556
+ }
557
+ /** Una sola petición en vuelo: las llamadas concurrentes esperan la misma promesa. */
558
+ #refresh(reason) {
559
+ this.#inFlight ??= this.#obtain(reason).finally(() => {
560
+ this.#inFlight = void 0;
561
+ });
562
+ return this.#inFlight;
563
+ }
564
+ /** Token del store si sigue vigente (otro proceso pudo renovarlo); si no, uno nuevo. */
565
+ async #obtain(reason) {
566
+ const stored = await this.#readStore();
567
+ if (stored !== void 0 && stored.accessToken !== this.#rejected && !this.#needsRefresh(stored)) {
568
+ this.#token = stored;
569
+ this.#invalidated = false;
570
+ return stored;
571
+ }
572
+ const token = await this.fetchToken();
573
+ this.#token = token;
574
+ this.#invalidated = false;
575
+ this.#rejected = void 0;
576
+ this.#log.emit("info", "sabre.auth.token.refresh", {
577
+ authVersion: this.#config.authVersion,
578
+ reason,
579
+ expiresIn: token.expiresIn
580
+ });
581
+ await this.#writeStore(token);
582
+ return token;
583
+ }
584
+ async #readStore() {
585
+ if (this.#store === void 0) return void 0;
586
+ try {
587
+ const value = await this.#store.get(await this.#key());
588
+ if (!isStoredToken(value)) return void 0;
589
+ return {
590
+ accessToken: value.accessToken,
591
+ expiresIn: (value.expiresAt - value.obtainedAt) / 1e3,
592
+ obtainedAt: value.obtainedAt
593
+ };
594
+ } catch (error) {
595
+ this.#storeFailed("get", error);
596
+ return void 0;
597
+ }
598
+ }
599
+ async #writeStore(token) {
600
+ if (this.#store === void 0) return;
601
+ const stored = {
602
+ accessToken: token.accessToken,
603
+ obtainedAt: token.obtainedAt,
604
+ expiresAt: token.obtainedAt + token.expiresIn * 1e3
605
+ };
606
+ try {
607
+ await this.#store.set(await this.#key(), stored);
608
+ } catch (error) {
609
+ this.#storeFailed("set", error);
610
+ }
611
+ }
612
+ #key() {
613
+ const { environment, authVersion, credentials } = this.#config;
614
+ this.#storeKey ??= tokenStoreKey(environment, authVersion, credentials);
615
+ return this.#storeKey;
202
616
  }
203
617
  /**
204
- * Procesa tickets proporcionando una solicitud que contiene una lista de números de ticket que se verificarán.
205
- * Actualmente, los EMD no son compatibles.
206
- *
207
- * payload - Contiene elementos obligatorios y opcionales para consultar los tickets.
208
- * @param payload CheckTicketsOptions
209
- * @returns {Promise<CheckTicketsResponseSuccess>} CheckTicketsResponse
618
+ * El store nunca rompe la llamada: se sigue con el caché en memoria. Del error solo se
619
+ * loguea el nombre: el mensaje lo arma el store del consumidor y podría traer el token.
210
620
  */
211
- async check(payload) {
212
- const path = "/v1/trip/orders/checkFlightTickets";
213
- return this.sabre.post(path, payload);
621
+ #storeFailed(operation, error) {
622
+ this.#log.emit("warn", "sabre.auth.token.store", {
623
+ authVersion: this.#config.authVersion,
624
+ operation,
625
+ errorName: error instanceof Error ? error.name : typeof error
626
+ });
214
627
  }
215
628
  /**
216
- * Procesa la emisión de billetes y documentos varios electrónicos (EMD).
217
- * Actualmente, los EMD no son compatibles.
218
- *
219
- * payload - Contiene elementos obligatorios y opcionales para la emisión de billetes.
220
- * @param payload FulfillTicketsOptions
221
- * @returns {Promise<FulfillTicketsResponseSuccess>}
629
+ * Pide un token nuevo. Los errores del endpoint (`invalid_request`/`invalid_client` 401,
630
+ * `unauthorized` 403) salen como `SabreGatewayError` con `retryable: false`.
222
631
  */
223
- async fulfill(payload) {
224
- const path = "/v1/trip/orders/fulfillFlightTickets";
225
- return this.sabre.post(path, payload);
632
+ async fetchToken() {
633
+ const { path, authorization, body } = buildTokenRequest(this.#config.authVersion, this.#config.credentials);
634
+ const response = await this.#transport.request({
635
+ method: "POST",
636
+ path,
637
+ service: TOKEN_SERVICE,
638
+ // Pedir otro token no tiene efectos duplicados: 429/5xx entran en la política genérica.
639
+ idempotent: true,
640
+ auth: false,
641
+ headers: { Authorization: authorization },
642
+ body
643
+ });
644
+ return this.#parse(response);
645
+ }
646
+ #parse(response) {
647
+ const accessToken = response?.access_token;
648
+ const expiresIn = response?.expires_in;
649
+ if (typeof accessToken !== "string" || accessToken === "" || typeof expiresIn !== "number" || !(expiresIn > 0)) {
650
+ throw new SabreGatewayError(`${TOKEN_SERVICE}: respuesta sin access_token/expires_in v\xE1lidos`, {
651
+ protocol: "rest",
652
+ status: 200,
653
+ retryable: false,
654
+ raw: response
655
+ });
656
+ }
657
+ return { accessToken, expiresIn, obtainedAt: this.#now() };
226
658
  }
227
659
  };
228
660
 
229
- // package.json
230
- var version = "0.2.4";
661
+ // src/booking/booking-errors.ts
662
+ var WARNING = "WARNING";
663
+ function responseErrors(response) {
664
+ return Array.isArray(response.errors) ? response.errors.filter(isObject2) : [];
665
+ }
666
+ function isWarning(item) {
667
+ return typeof item.category === "string" && item.category.toUpperCase() === WARNING;
668
+ }
669
+ function toWarning(service, item) {
670
+ return new SabreWarning(messageOf(service, item), { service, code: codeOf(item), raw: item });
671
+ }
672
+ function toBusinessError(service, item, { warnings = [], errors = [], retryable = false } = {}) {
673
+ return new SabreBusinessError(messageOf(service, item), {
674
+ service,
675
+ code: codeOf(item),
676
+ raw: item,
677
+ warnings,
678
+ errors,
679
+ retryable
680
+ });
681
+ }
682
+ function codeOf(item) {
683
+ return typeof item.type === "string" && item.type !== "" ? item.type : String(item.category);
684
+ }
685
+ function messageOf(service, item) {
686
+ const where = typeof item.fieldPath === "string" && item.fieldPath !== "" ? ` en ${item.fieldPath}` : "";
687
+ const description = typeof item.description === "string" && item.description !== "" ? `: ${item.description}` : "";
688
+ return `${service}: ${String(item.category)} ${codeOf(item)}${where}${description}`;
689
+ }
690
+ function nonEmpty(value) {
691
+ return typeof value === "string" && value !== "";
692
+ }
693
+ function isObject2(value) {
694
+ return typeof value === "object" && value !== null;
695
+ }
231
696
 
232
- // src/config.ts
233
- var defaultBaseUrl = "https://api.platform.sabre.com";
234
- var defaultUserAgent = `sabre-client-node:${version}`;
235
- var defaultConversationId = `sabre.client.node:${version}`;
236
- var defaultDomain = "AA";
237
- var baseUrl = typeof process !== "undefined" && process.env ? process.env.SABRE_REST_URL || defaultBaseUrl : defaultBaseUrl;
238
- var userAgent = typeof process !== "undefined" && process.env ? process.env.SABRE_USER_AGENT || defaultUserAgent : defaultUserAgent;
239
- var domain = typeof process !== "undefined" && process.env ? process.env.SABRE_DOMAIN || defaultDomain : defaultDomain;
240
- var conversationId = typeof process !== "undefined" && process.env ? process.env.SABRE_CONVERSATION_ID || defaultConversationId : defaultConversationId;
697
+ // src/booking/cancel-booking.ts
698
+ var CANCEL_BOOKING_SERVICE = "cancelBooking";
699
+ var PATH3 = "/v1/trip/orders/cancelBooking";
700
+ var NO_ITEMS_CANCELLED = "NO_ITEMS_CANCELLED";
701
+ var ITEM_LISTS = ["flights", "hotels", "cars", "trains", "cruises", "segments"];
702
+ async function cancelBooking(transport, request, options = {}) {
703
+ validateCancelBookingRequest(request);
704
+ const body = await transport.request({
705
+ method: "POST",
706
+ path: PATH3,
707
+ service: CANCEL_BOOKING_SERVICE,
708
+ idempotent: false,
709
+ body: request,
710
+ classify: classifyCancelBooking,
711
+ ...options.signal === void 0 ? {} : { signal: options.signal }
712
+ });
713
+ return toCancelBookingResponse(body);
714
+ }
715
+ function validateCancelBookingRequest(request) {
716
+ if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
717
+ throw new TypeError("sabre-client: booking.cancel espera { confirmationId: string, ... }");
718
+ }
719
+ const lists = ITEM_LISTS.join(", ");
720
+ if (request.cancelAll === true) {
721
+ if (ITEM_LISTS.some((list) => request[list] !== void 0)) {
722
+ throw new TypeError(`sabre-client: booking.cancel no combina cancelAll con listas (${lists})`);
723
+ }
724
+ } else if (!ITEM_LISTS.some((list) => Array.isArray(request[list]) && request[list].length > 0)) {
725
+ throw new TypeError(`sabre-client: booking.cancel necesita cancelAll: true o al menos una lista (${lists})`);
726
+ }
727
+ if (request.offerItemId !== void 0 && request.flightTicketOperation !== void 0) {
728
+ throw new TypeError("sabre-client: booking.cancel no combina offerItemId con flightTicketOperation");
729
+ }
730
+ }
731
+ function classifyCancelBooking(body) {
732
+ if (!isObject2(body)) {
733
+ return new SabreError(`${CANCEL_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
734
+ retryable: false,
735
+ raw: body
736
+ });
737
+ }
738
+ const items = responseErrors(body);
739
+ const nothingCancelled = items.find(isNoItemsCancelled);
740
+ const warnings = items.filter((item) => isWarning(item) && !isNoItemsCancelled(item)).map(warningOf);
741
+ const fatal = items.filter((item) => !isWarning(item));
742
+ if (nothingCancelled !== void 0) {
743
+ return toBusinessError(CANCEL_BOOKING_SERVICE, nothingCancelled, { warnings, errors: fatal.map(errorOf) });
744
+ }
745
+ const [first, ...rest] = fatal;
746
+ if (first === void 0) return void 0;
747
+ return toBusinessError(CANCEL_BOOKING_SERVICE, first, { warnings, errors: rest.map(errorOf) });
748
+ }
749
+ function toCancelBookingResponse(body) {
750
+ const { errors: _errors, ...rest } = body;
751
+ return { ...rest, warnings: responseErrors(body).map(warningOf) };
752
+ }
753
+ function isNoItemsCancelled(item) {
754
+ return item.type === NO_ITEMS_CANCELLED;
755
+ }
756
+ function warningOf(item) {
757
+ return toWarning(CANCEL_BOOKING_SERVICE, item);
758
+ }
759
+ function errorOf(item) {
760
+ return toBusinessError(CANCEL_BOOKING_SERVICE, item);
761
+ }
241
762
 
242
- // src/errors.ts
243
- var SabreError = class extends Error {
244
- details;
245
- statusCode;
246
- constructor(message, statusCode, details) {
247
- super(message);
248
- this.name = "SabreError";
249
- this.statusCode = statusCode;
250
- this.details = details;
763
+ // src/booking/create-booking.ts
764
+ var CREATE_BOOKING_SERVICE = "createBooking";
765
+ var PATH4 = "/v1/trip/orders/createBooking";
766
+ var UNABLE_TO_RETRIEVE_BOOKING = "UNABLE_TO_RETRIEVE_BOOKING";
767
+ async function createBooking(transport, request, options = {}) {
768
+ if (typeof request !== "object" || request === null) {
769
+ throw new TypeError("sabre-client: booking.create espera un objeto CreateBookingRequest");
251
770
  }
252
- };
771
+ const body = await transport.request({
772
+ method: "POST",
773
+ path: PATH4,
774
+ service: CREATE_BOOKING_SERVICE,
775
+ idempotent: false,
776
+ body: request,
777
+ classify: classifyCreateBooking,
778
+ ...options.signal === void 0 ? {} : { signal: options.signal }
779
+ });
780
+ return toCreateBookingResponse(body);
781
+ }
782
+ function classifyCreateBooking(body) {
783
+ if (!isObject2(body)) {
784
+ return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
785
+ retryable: false,
786
+ raw: body
787
+ });
788
+ }
789
+ const response = body;
790
+ if (confirmationIdOf(response) !== void 0) return void 0;
791
+ if (reservationWithoutId(response)) {
792
+ return new SabreError(
793
+ `${CREATE_BOOKING_SERVICE}: Sabre indica que la reserva se cre\xF3, pero no devolvi\xF3 su confirmationId`,
794
+ { retryable: false, raw: body }
795
+ );
796
+ }
797
+ const items = responseErrors(response);
798
+ const fatal = items.find((item) => !isWarning(item));
799
+ if (fatal !== void 0) return toBusinessError(CREATE_BOOKING_SERVICE, fatal, { warnings: items.filter(isWarning).map(warningOf2) });
800
+ return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, sin confirmationId ni errores fatales`, {
801
+ retryable: false,
802
+ raw: body
803
+ });
804
+ }
805
+ function toCreateBookingResponse(body) {
806
+ const items = responseErrors(body);
807
+ return {
808
+ confirmationId: confirmationIdOf(body),
809
+ ...body.timestamp === void 0 ? {} : { timestamp: body.timestamp },
810
+ ...body.booking === void 0 ? {} : { booking: body.booking },
811
+ ...body.request === void 0 ? {} : { request: body.request },
812
+ warnings: items.filter(isWarning).map(warningOf2),
813
+ errors: items.filter((item) => !isWarning(item)).map((item) => toBusinessError(CREATE_BOOKING_SERVICE, item))
814
+ };
815
+ }
816
+ function confirmationIdOf(response) {
817
+ if (nonEmpty(response.confirmationId)) return response.confirmationId;
818
+ if (nonEmpty(response.booking?.bookingId)) return response.booking.bookingId;
819
+ const notRetrieved = responseErrors(response).find((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
820
+ return notRetrieved === void 0 ? void 0 : locatorIn(notRetrieved.description);
821
+ }
822
+ function reservationWithoutId(response) {
823
+ return response.booking !== void 0 || responseErrors(response).some((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
824
+ }
825
+ function locatorIn(description) {
826
+ if (description === void 0) return void 0;
827
+ return /\(([A-Z0-9]{6,})\)/.exec(description)?.[1] ?? /\b([A-Z0-9]{6,})\b/.exec(description)?.[1];
828
+ }
829
+ function warningOf2(item) {
830
+ return toWarning(CREATE_BOOKING_SERVICE, item);
831
+ }
832
+
833
+ // src/booking/get-booking.ts
834
+ var GET_BOOKING_SERVICE = "getBooking";
835
+ var PATH5 = "/v1/trip/orders/getBooking";
836
+ var RETRYABLE_TYPES = /* @__PURE__ */ new Set(["INTERNAL_SERVER_TIMEOUT", "DOWNLINE_SERVICE_FAILURE"]);
837
+ async function getBooking(transport, request, options = {}) {
838
+ if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
839
+ throw new TypeError("sabre-client: booking.get espera { confirmationId: string, ... }");
840
+ }
841
+ const body = await transport.request({
842
+ method: "POST",
843
+ path: PATH5,
844
+ service: GET_BOOKING_SERVICE,
845
+ idempotent: true,
846
+ body: request,
847
+ classify: classifyGetBooking,
848
+ ...options.signal === void 0 ? {} : { signal: options.signal }
849
+ });
850
+ return toGetBookingResponse(body);
851
+ }
852
+ function classifyGetBooking(body) {
853
+ if (!isObject2(body)) {
854
+ return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
855
+ retryable: false,
856
+ raw: body
857
+ });
858
+ }
859
+ const response = body;
860
+ if (nonEmpty(response.bookingId)) return void 0;
861
+ const items = responseErrors(response);
862
+ const fatal = items.find((item) => !isWarning(item));
863
+ if (fatal !== void 0) return errorOf2(fatal, items.filter(isWarning).map(warningOf3));
864
+ return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, sin bookingId ni errores fatales`, {
865
+ retryable: false,
866
+ raw: body
867
+ });
868
+ }
869
+ function toGetBookingResponse(body) {
870
+ const items = responseErrors(body);
871
+ return {
872
+ ...body,
873
+ bookingId: body.bookingId,
874
+ warnings: items.filter(isWarning).map(warningOf3),
875
+ errors: items.filter((item) => !isWarning(item)).map((item) => errorOf2(item, []))
876
+ };
877
+ }
878
+ function warningOf3(item) {
879
+ return toWarning(GET_BOOKING_SERVICE, item);
880
+ }
881
+ function errorOf2(item, warnings) {
882
+ return toBusinessError(GET_BOOKING_SERVICE, item, { warnings, retryable: RETRYABLE_TYPES.has(item.type) });
883
+ }
253
884
 
254
- // src/trip-management/reservation.ts
255
- var Reservation = class {
885
+ // src/booking/booking-management.ts
886
+ var BookingManagement = class {
887
+ #transport;
888
+ /** @internal Lo construye el cliente `Sabre`. */
889
+ constructor(transport) {
890
+ this.#transport = transport;
891
+ }
256
892
  /**
257
- * Reservation
258
- *
259
- * La API para crear un registro de nombre de pasajero es una API
260
- * orquestada que integra varias funciones y operaciones en una sola llamada.
261
- *
262
- * Esta API permite crear un registro de nombre de pasajero (PNR),
263
- * incluyendo segmentos aéreos, en una sola llamada.
264
- *
265
- * @param sabre Sabre Client Reference
893
+ * Create Booking: crea la reserva. Para un itinerario de BFM/Revalidate, `flightDetails` se
894
+ * arma con `buildBookingFlightDetails`.
895
+ *
896
+ * Nunca se reintenta automáticamente. Si la reserva se crea con avisos o errores no fatales,
897
+ * no lanza: los devuelve en `warnings` y `errors`. Si no se crea, lanza `SabreBusinessError`.
266
898
  */
267
- constructor(sabre) {
268
- this.sabre = sabre;
899
+ create(request, options) {
900
+ return createBooking(this.#transport, request, options);
269
901
  }
270
902
  /**
271
- * Cree una reserva completa de contenido aéreo, hotel y automóvil de alquiler, todo dentro de una sola solicitud.
272
- *
273
- * payload - Contiene elementos obligatorios y opcionales para realizar una solicitud de reserva.
274
- * @param payload CreatePassengerNameRecordOptions
275
- * @returns {Promise<CreatePassengerNameRecordResponseSuccess>}
903
+ * Get Booking: lee la reserva por `confirmationId`. El `Booking` va en el primer nivel de la
904
+ * respuesta, a diferencia de `create`. Usa `returnOnly` para pedir solo las secciones que
905
+ * hacen falta: además acelera la respuesta.
906
+ *
907
+ * Si la reserva existe, no lanza: los avisos y errores parciales van en `warnings` y
908
+ * `errors`. Si no existe o no se pudo leer (p. ej. `BOOKING_NOT_FOUND`), lanza
909
+ * `SabreBusinessError`. Es una lectura: se reintenta ante fallos transitorios.
276
910
  */
277
- create(payload) {
278
- const path = "/v2.5.0/passenger/records?mode=create";
279
- return this.sabre.post(path, payload);
911
+ get(request, options) {
912
+ return getBooking(this.#transport, request, options);
913
+ }
914
+ /**
915
+ * Cancel Booking: cancela la reserva entera (`cancelAll: true`) o por `itemId` (p. ej. los
916
+ * `flights[].itemId` de `get`). Valida antes de enviar las combinaciones que Sabre rechaza y,
917
+ * si encuentra una, lanza `TypeError` sin llamar a la red.
918
+ *
919
+ * Nunca se reintenta automáticamente. Lanza `SabreBusinessError` si algo falló o si Sabre
920
+ * responde `NO_ITEMS_CANCELLED` (no se canceló nada), aunque llegue como aviso. Con
921
+ * `END_TRANSACTION_PROBLEM` en `warnings`, la confirmación quedó pendiente: verifica con `get`.
922
+ */
923
+ cancel(request, options) {
924
+ return cancelBooking(this.#transport, request, options);
280
925
  }
281
926
  };
282
927
 
283
- // src/sabre.ts
284
- var Sabre = class {
285
- constructor(options = {}) {
286
- this.options = options;
287
- const processEnv = typeof process !== "undefined" && process.env;
288
- if (!this.options.username && processEnv)
289
- this.options.username = processEnv.SABRE_USERNAME;
290
- if (!this.options.password && processEnv)
291
- this.options.password = processEnv.SABRE_PASSWORD;
292
- if (!this.options.organization && processEnv)
293
- this.options.organization = processEnv.SABRE_ORGANIZATION;
294
- if (!this.options.username || !this.options.password || !this.options.organization) {
295
- throw new Error(`Missing Sabre authorization. Pass it to the constructor new Sabre({
296
- username: '773400',
297
- password: 'PASSWORD_GOES_HERE',
298
- organization: '7TZA', // pcc
299
- });})`);
928
+ // src/http/retry.ts
929
+ var DEFAULT_RETRY = Object.freeze({ maxRetries: 3, baseDelayMs: 500 });
930
+ var MIN_RETRY_DELAY_MS = 500;
931
+ function retryDelay(retry, baseDelayMs, random = Math.random) {
932
+ return Math.max(MIN_RETRY_DELAY_MS, Math.floor(random() * baseDelayMs * 2 ** retry));
933
+ }
934
+ function shouldRetry(error, idempotent) {
935
+ return idempotent && error instanceof SabreError && error.retryable;
936
+ }
937
+ function validateRetryOptions(retry, where) {
938
+ if (retry.maxRetries !== void 0 && !(Number.isInteger(retry.maxRetries) && retry.maxRetries >= 0)) {
939
+ throw new TypeError(`sabre-client: ${where}.maxRetries debe ser un entero >= 0`);
940
+ }
941
+ if (retry.baseDelayMs !== void 0 && !(Number.isFinite(retry.baseDelayMs) && retry.baseDelayMs >= 0)) {
942
+ throw new TypeError(`sabre-client: ${where}.baseDelayMs debe ser un n\xFAmero >= 0`);
943
+ }
944
+ }
945
+
946
+ // src/config.ts
947
+ var BASE_URLS = Object.freeze({
948
+ sandbox: "https://api.cert.platform.sabre.com",
949
+ production: "https://api.platform.sabre.com"
950
+ });
951
+ var LOGGER_METHODS = ["debug", "info", "warn", "error"];
952
+ function fail(message) {
953
+ throw new TypeError(`sabre-client: ${message}`);
954
+ }
955
+ function isObject3(value) {
956
+ return typeof value === "object" && value !== null;
957
+ }
958
+ function requireString(value, field) {
959
+ if (typeof value !== "string" || value.trim() === "") fail(`${field} es requerido y debe ser un string no vac\xEDo`);
960
+ }
961
+ function resolveConfig(config) {
962
+ if (!isObject3(config)) fail("la config debe ser un objeto");
963
+ const { environment } = config;
964
+ if (environment !== "sandbox" && environment !== "production") {
965
+ fail(`environment debe ser 'sandbox' o 'production' (recibido: ${JSON.stringify(environment)})`);
966
+ }
967
+ const authVersion = config.authVersion ?? "v3";
968
+ if (authVersion !== "v3" && authVersion !== "v2") {
969
+ fail(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(authVersion)})`);
970
+ }
971
+ const { credentials } = config;
972
+ if (!isObject3(credentials)) fail("credentials es requerido");
973
+ requireString(credentials.username, "credentials.username");
974
+ requireString(credentials.password, "credentials.password");
975
+ if (authVersion === "v3") {
976
+ requireString(credentials.clientId, "credentials.clientId (requerido con authVersion v3)");
977
+ requireString(credentials.clientSecret, "credentials.clientSecret (requerido con authVersion v3)");
978
+ }
979
+ if (config.pcc !== void 0) requireString(config.pcc, "pcc");
980
+ const { logger } = config;
981
+ if (logger !== void 0) {
982
+ if (!isObject3(logger)) fail("logger debe ser un objeto con debug/info/warn/error");
983
+ for (const method of LOGGER_METHODS) {
984
+ if (typeof logger[method] !== "function") fail(`logger.${method} debe ser una funci\xF3n`);
300
985
  }
301
- this.headers = new Headers({
302
- "User-Agent": userAgent,
303
- "Conversation-ID": conversationId
304
- });
305
- this.secret = this.getSecret();
306
- }
307
- headers;
308
- secret;
309
- authentication = new Authentication(this);
310
- booking = new Booking(this);
311
- flightTickets = new FlightTickets(this);
312
- search = new Search(this);
313
- ticketing = new Ticketing(this);
314
- reservation = new Reservation(this);
315
- setAuthorization(token) {
316
- this.headers.set("Authorization", `Bearer ${token}`);
317
- this.headers.set("Content-Type", "application/json");
318
- }
319
- async fetchRequest(path, options = {}) {
320
- const response = await fetch(`${baseUrl}${path}`, options);
321
- if (!response.ok) {
322
- let errorDetails = `Request failed with status ${response.status}`;
986
+ }
987
+ if (config.retry !== void 0) {
988
+ if (!isObject3(config.retry)) fail("retry debe ser un objeto");
989
+ validateRetryOptions(config.retry, "retry");
990
+ }
991
+ const { tokenStore } = config;
992
+ if (tokenStore !== void 0) {
993
+ if (!isObject3(tokenStore) || typeof tokenStore.get !== "function" || typeof tokenStore.set !== "function") {
994
+ fail("tokenStore debe ser un objeto con get(key) y set(key, token)");
995
+ }
996
+ }
997
+ const fetchImpl = config.fetch ?? globalThis.fetch;
998
+ if (typeof fetchImpl !== "function") fail("no hay fetch disponible: pasa config.fetch");
999
+ return {
1000
+ credentials: Object.freeze({ ...credentials }),
1001
+ environment,
1002
+ baseUrl: BASE_URLS[environment],
1003
+ authVersion,
1004
+ pcc: config.pcc,
1005
+ logger,
1006
+ retry: Object.freeze({ ...DEFAULT_RETRY, ...definedOnly(config.retry ?? {}) }),
1007
+ // Enlazado al global para evitar "Illegal invocation" en algunos runtimes.
1008
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1009
+ tokenStore
1010
+ };
1011
+ }
1012
+ function definedOnly(value) {
1013
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1014
+ }
1015
+
1016
+ // src/http/rest-transport.ts
1017
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 503, 504]);
1018
+ var RestTransport = class {
1019
+ #config;
1020
+ #log;
1021
+ #random;
1022
+ #sleep;
1023
+ #authorization;
1024
+ constructor(config, options = {}) {
1025
+ this.#config = config;
1026
+ this.#log = new EventLogger(config.logger);
1027
+ this.#authorization = options.authorization;
1028
+ this.#random = options.random ?? Math.random;
1029
+ this.#sleep = options.sleep ?? sleep;
1030
+ }
1031
+ /** Punto de enganche para `AuthManager`. */
1032
+ setAuthorizationProvider(provider) {
1033
+ this.#authorization = provider;
1034
+ }
1035
+ async request(request) {
1036
+ if (request.retry !== void 0) validateRetryOptions(request.retry, "retry");
1037
+ const retry = { ...this.#config.retry, ...definedOnly(request.retry ?? {}) };
1038
+ let reauthorized = false;
1039
+ for (let attempt = 0; ; attempt++) {
1040
+ const authorization = await this.#authorizationFor(request);
323
1041
  try {
324
- const jsonError = await response.json();
325
- errorDetails = JSON.stringify(jsonError);
326
- } catch (e) {
327
- console.error(e);
328
- errorDetails = response.statusText;
1042
+ return await this.#send(request, attempt, authorization);
1043
+ } catch (error) {
1044
+ if (authorization !== void 0 && !reauthorized && isUnauthorized(error)) {
1045
+ reauthorized = true;
1046
+ this.#authorization?.invalidate(authorization);
1047
+ this.#log.emit("warn", "sabre.request.retry", {
1048
+ service: request.service,
1049
+ method: request.method,
1050
+ path: request.path,
1051
+ attempt: attempt + 1,
1052
+ reason: "unauthorized",
1053
+ error
1054
+ });
1055
+ attempt--;
1056
+ continue;
1057
+ }
1058
+ if (attempt >= retry.maxRetries || !shouldRetry(error, request.idempotent)) throw error;
1059
+ const delayMs = retryDelay(attempt, retry.baseDelayMs, this.#random);
1060
+ this.#log.emit("warn", "sabre.request.retry", {
1061
+ service: request.service,
1062
+ method: request.method,
1063
+ path: request.path,
1064
+ attempt: attempt + 1,
1065
+ maxRetries: retry.maxRetries,
1066
+ delayMs,
1067
+ error
1068
+ });
1069
+ await this.#sleep(delayMs, request.signal);
329
1070
  }
330
- throw new SabreError(
331
- `Request failed with status ${response.status}`,
332
- response.status,
333
- errorDetails
334
- );
335
1071
  }
336
- const data = await response.json();
337
- return data;
338
1072
  }
339
- async post(path, entity, options = {}) {
340
- const requestOptions = {
341
- method: "POST",
342
- headers: this.headers,
343
- body: JSON.stringify(entity),
344
- ...options
345
- };
346
- return this.fetchRequest(path, requestOptions);
1073
+ async #authorizationFor(request) {
1074
+ if (request.auth === false || this.#authorization === void 0) return void 0;
1075
+ return this.#authorization.get();
347
1076
  }
348
- async auth(path, entity, options = {}) {
349
- this.headers.set("Authorization", `Basic ${this.secret}`);
350
- this.headers.set("Content-Type", "application/x-www-form-urlencoded");
351
- const requestOptions = {
352
- method: "POST",
353
- headers: this.headers,
354
- body: entity,
355
- ...options
1077
+ async #send(request, attempt, authorization) {
1078
+ const url = this.#url(request);
1079
+ const headers = new Headers({ Accept: "application/json", ...request.headers });
1080
+ const form = request.body instanceof URLSearchParams;
1081
+ if (request.body !== void 0 && !headers.has("Content-Type")) {
1082
+ headers.set("Content-Type", form ? "application/x-www-form-urlencoded" : "application/json");
1083
+ }
1084
+ if (authorization !== void 0) headers.set("Authorization", authorization);
1085
+ const logContext = { service: request.service, method: request.method, path: request.path, attempt };
1086
+ this.#log.emit("debug", "sabre.request.start", logContext);
1087
+ const startedAt = Date.now();
1088
+ let response;
1089
+ try {
1090
+ response = await this.#config.fetch(url, {
1091
+ method: request.method,
1092
+ headers,
1093
+ ...request.body === void 0 ? {} : { body: form ? String(request.body) : JSON.stringify(request.body) },
1094
+ ...request.signal === void 0 ? {} : { signal: request.signal }
1095
+ });
1096
+ } catch (cause) {
1097
+ if (isAbort(cause, request.signal)) throw cause;
1098
+ const error = new SabreGatewayError(`${request.service}: fallo de red (${describe(cause)})`, {
1099
+ protocol: "rest",
1100
+ retryable: true,
1101
+ cause
1102
+ });
1103
+ this.#log.emit("error", "sabre.request.end", { ...logContext, durationMs: Date.now() - startedAt, error });
1104
+ throw error;
1105
+ }
1106
+ const body = await readBody(response);
1107
+ const endContext = { ...logContext, status: response.status, durationMs: Date.now() - startedAt };
1108
+ if (!response.ok) {
1109
+ const error = gatewayError(request.service, response.status, body);
1110
+ this.#log.emit("error", "sabre.request.end", { ...endContext, error });
1111
+ throw error;
1112
+ }
1113
+ const serviceError = request.classify?.(body, response);
1114
+ if (serviceError !== void 0) {
1115
+ this.#log.emit("warn", "sabre.request.end", { ...endContext, error: serviceError });
1116
+ throw serviceError;
1117
+ }
1118
+ this.#log.emit("info", "sabre.request.end", endContext);
1119
+ return body;
1120
+ }
1121
+ #url(request) {
1122
+ const path = request.path.startsWith("/") ? request.path : `/${request.path}`;
1123
+ const url = new URL(path, this.#config.baseUrl);
1124
+ for (const [key, value] of Object.entries(request.query ?? {})) {
1125
+ if (value !== void 0) url.searchParams.set(key, String(value));
1126
+ }
1127
+ return url.toString();
1128
+ }
1129
+ };
1130
+ async function readBody(response) {
1131
+ const text = await response.text();
1132
+ if (text === "") return void 0;
1133
+ try {
1134
+ return JSON.parse(text);
1135
+ } catch {
1136
+ return text;
1137
+ }
1138
+ }
1139
+ function gatewayError(service, status, body) {
1140
+ const code = extractCode(body);
1141
+ return new SabreGatewayError(`${service}: HTTP ${status}${code === void 0 ? "" : ` ${code}`}`, {
1142
+ protocol: "rest",
1143
+ status,
1144
+ retryable: RETRYABLE_STATUS.has(status),
1145
+ raw: body,
1146
+ ...code === void 0 ? {} : { code }
1147
+ });
1148
+ }
1149
+ function extractCode(body) {
1150
+ if (typeof body !== "object" || body === null) return void 0;
1151
+ for (const key of ["errorCode", "error"]) {
1152
+ const value = body[key];
1153
+ if (typeof value === "string" && value !== "") return value;
1154
+ }
1155
+ return void 0;
1156
+ }
1157
+ function isUnauthorized(error) {
1158
+ return error instanceof SabreGatewayError && error.protocol === "rest" && error.status === 401;
1159
+ }
1160
+ function isAbort(error, signal) {
1161
+ return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
1162
+ }
1163
+ function describe(error) {
1164
+ return error instanceof Error ? error.message : String(error);
1165
+ }
1166
+ function sleep(ms, signal) {
1167
+ return new Promise((resolve, reject) => {
1168
+ if (signal?.aborted) return reject(signal.reason);
1169
+ const timer = setTimeout(() => {
1170
+ signal?.removeEventListener("abort", onAbort);
1171
+ resolve();
1172
+ }, ms);
1173
+ const onAbort = () => {
1174
+ clearTimeout(timer);
1175
+ reject(signal?.reason);
356
1176
  };
357
- return this.fetchRequest(path, requestOptions);
1177
+ signal?.addEventListener("abort", onAbort, { once: true });
1178
+ });
1179
+ }
1180
+
1181
+ // src/sabre.ts
1182
+ var Sabre = class {
1183
+ #config;
1184
+ #transport;
1185
+ /** Servicios de Air: shopping, revalidación. */
1186
+ air;
1187
+ /** Booking Management: creación de reservas. */
1188
+ booking;
1189
+ constructor(config) {
1190
+ this.#config = resolveConfig(config);
1191
+ this.#transport = new RestTransport(this.#config);
1192
+ const auth = new AuthManager(this.#config, this.#transport);
1193
+ this.#transport.setAuthorizationProvider(auth.bearerProvider());
1194
+ this.air = new Air(this.#transport);
1195
+ this.booking = new BookingManagement(this.#transport);
358
1196
  }
359
- getSecret() {
360
- if (!this.options.password) throw new Error("Missing Sabre password");
361
- return btoa(`${this.userID()}:${btoa(this.options.password)}`);
1197
+ get environment() {
1198
+ return this.#config.environment;
362
1199
  }
363
- userID() {
364
- return btoa(`V1:${this.options.username}:${this.options.organization}:${domain}`);
1200
+ get baseUrl() {
1201
+ return this.#config.baseUrl;
365
1202
  }
366
1203
  };
1204
+
1205
+ // src/booking/flight-details.ts
1206
+ function buildBookingFlightDetails(source) {
1207
+ const { group, itinerary, priceComparison } = source;
1208
+ const flights = itineraryFlights(group, itinerary).map(
1209
+ ({ schedule, departureDate, bookingCode }) => ({
1210
+ flightNumber: schedule.carrier.marketingFlightNumber,
1211
+ airlineCode: schedule.carrier.marketing,
1212
+ fromAirportCode: schedule.departure.airport,
1213
+ toAirportCode: schedule.arrival.airport,
1214
+ departureDate,
1215
+ departureTime: schedule.departure.time.slice(0, 5),
1216
+ bookingClass: bookingCode,
1217
+ flightStatusCode: "NN"
1218
+ })
1219
+ );
1220
+ const fare = itinerary.pricingInformation[0]?.fare;
1221
+ if (fare === void 0) {
1222
+ throw new TypeError("sabre-client: el itinerario no trae pricingInformation");
1223
+ }
1224
+ const passengersPricing = fare.passengerInfoList.map(
1225
+ ({ passengerInfo }) => ({
1226
+ passengerCode: passengerInfo.passengerType,
1227
+ numberOfpassengers: passengerInfo.passengerNumber ?? 1
1228
+ })
1229
+ );
1230
+ let priceComparisons;
1231
+ if (priceComparison !== void 0) {
1232
+ const total = fare.totalFare?.totalPrice;
1233
+ if (total === void 0) {
1234
+ throw new TypeError("sabre-client: el itinerario no trae totalFare.totalPrice para priceComparison");
1235
+ }
1236
+ priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
1237
+ }
1238
+ return {
1239
+ flights,
1240
+ flightPricing: [
1241
+ {
1242
+ ...priceComparisons === void 0 ? {} : { priceComparisons },
1243
+ qualifiers: {
1244
+ passengersPricing,
1245
+ ...fare.validatingCarrierCode === void 0 ? {} : { validatingAirlineCode: fare.validatingCarrierCode }
1246
+ }
1247
+ }
1248
+ ]
1249
+ };
1250
+ }
1251
+ function amount(value) {
1252
+ return String(Math.round(value * 1e3) / 1e3);
1253
+ }
1254
+
1255
+ // src/index.ts
1256
+ var VERSION = "0.3.0";
367
1257
  export {
368
- Sabre
1258
+ Sabre,
1259
+ SabreBusinessError,
1260
+ SabreError,
1261
+ SabreGatewayError,
1262
+ SabreOrchestrationError,
1263
+ SabreWarning,
1264
+ VERSION,
1265
+ buildBookingFlightDetails,
1266
+ buildRevalidateItineraryRequest
369
1267
  };
370
1268
  //# sourceMappingURL=index.js.map