sabre-client 0.4.0 → 0.5.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/README.es.md CHANGED
@@ -12,17 +12,31 @@ SDK de TypeScript para las APIs de Sabre.
12
12
 
13
13
  ## Estado
14
14
 
15
- `0.4.0` es la versión actual de la reescritura. Cubre el Tier 1 y el Tier 2 del flujo de Air
16
- Booking por REST, y el Tier 3 (sesiones SOAP y colas). Sobre la `0.3.0` agrega `buildCredentials`
17
- y los comandos de host (`session.command`), sin cambios incompatibles.
15
+ La `0.5.0` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.4.1`.
16
+ Cubre el Tier 1 y el Tier 2 del flujo de Air Booking por REST, el Tier 3 (sesiones SOAP y colas) y
17
+ el Tier 4: actividad de colas, historial del PNR, la cola de mensajes `S`, `selection` en `list` y
18
+ `onRawRequest`. Trae tres cambios incompatibles (ver [Actualizar desde la 0.4](#actualizar-desde-la-04)).
18
19
 
19
20
  ```sh
20
- npm install sabre-client
21
+ npm install sabre-client@next # 0.5.0
22
+ npm install sabre-client # 0.4.1
21
23
  ```
22
24
 
23
25
  El paquete sigue en `0.x`: una versión minor puede traer cambios incompatibles, y el
24
26
  [CHANGELOG](./CHANGELOG.md) los marca.
25
27
 
28
+ ### Actualizar desde la 0.4
29
+
30
+ - **`pcc` ya no está en la config.** Pasarlo a `new Sabre` o a `new SabreLegacy` da un error de
31
+ tipos, y en JavaScript se ignora. El PCC con el que te autenticas va en `credentials` (ver
32
+ [Credenciales](#credenciales)); otro PCC se pasa en cada operación, como `targetPcc` en REST o
33
+ `pcc` en las operaciones de colas.
34
+ - **`QueueRef` puede ser un `string`.** Una cola alfabética como `'S'` es `'S'`, y `count` y
35
+ `place` la devuelven así en lugar de `{ name: 'S' }`. El código que hace
36
+ `typeof q === 'number' ? … : q.name` tiene que manejar el caso `string`.
37
+ - **Un `QueueStep` que no terminó tiene `kind`.** Revisa `step.kind === 'pnr'` antes de leer
38
+ `step.item`; `kind: 'message'` trae un mensaje de la cola `S`.
39
+
26
40
  El Tier 1 cubre el flujo básico de Air Booking por REST:
27
41
 
28
42
  | Paso | Servicio | Método |
@@ -45,11 +59,12 @@ El Tier 3 agrega el cliente SOAP `SabreLegacy`, con una sesión stateful y Queue
45
59
  | Paso | Servicio | Método |
46
60
  | - | - | - |
47
61
  | Sesión | `SessionCreateRQ` 2.0.0 / 1.0.0, `OTA_PingRQ`, `SessionCloseRQ` | `legacy.withSession`, `legacy.openSession` |
48
- | Colas | Get Queue Count, Access Queue, Place Queue Message | `session.queue.count`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
49
- | Párrafo de cola | — (función pura) | `parseQueueParagraph`, `resolveHostDate` |
62
+ | Colas | Get Queue Count, Get Queue Activity, Access Queue, Place Queue Message | `session.queue.count`, `activity`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
63
+ | Párrafo de cola | — (función pura) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
64
+ | Historial del PNR | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
50
65
  | Comandos de host | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
51
66
 
52
- Planeado después de `0.4.0`: Get Ancillaries (solo NDC).
67
+ Planeado después de `0.5.0`: Get Ancillaries (solo NDC).
53
68
 
54
69
  ## Requisitos
55
70
 
@@ -115,6 +130,14 @@ const sabre = new Sabre({ environment: 'sandbox', credentials });
115
130
  const legacy = new SabreLegacy({ environment: 'sandbox', credentials });
116
131
  ```
117
132
 
133
+ El objeto que devuelve `buildCredentials` lleva su `authVersion`, así que la config no necesita
134
+ repetirlo: con `authVersion: 'v2'` en `buildCredentials`, `{ environment, credentials }` ya es una
135
+ config v2. Si también pasas `authVersion` en la config, tiene que coincidir; si no, el cliente lanza
136
+ un `TypeError`. Las credenciales que armas a mano no llevan `authVersion`, así que con v2 la config
137
+ sigue necesitando `authVersion: 'v2'`. Si la versión solo se conoce en runtime (por ejemplo, leída
138
+ de una variable de entorno a un `BuildCredentialsOptions`), el resultado de `buildCredentials`
139
+ también va directo a la config, sin `authVersion` en ella.
140
+
118
141
  El PCC de las credenciales es con el que te autenticas. Trabajar en otro PCC no requiere otro
119
142
  juego de credenciales: se pasa en cada operación, como `targetPcc` en `booking.create`, `get` y
120
143
  `cancel`, o como `pcc` en las operaciones de colas.
@@ -350,6 +373,7 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
350
373
  // screen.text → 'PSGR TYPE - ON' (como lo manda Sabre, sin recortar)
351
374
  // screen.warnings → los stl:Warning, si los hay
352
375
  // screen.messageId → eb:MessageId, para soporte de Sabre
376
+ // screen.respondedAt → eb:Timestamp en UTC, p. ej. '2026-11-06T17:34:16Z'
353
377
  ```
354
378
 
355
379
  - **El comando va tal cual.** En este servicio, el separador `‡` de Sabre se escribe `¥`
@@ -377,16 +401,63 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
377
401
  caracteres (`W/PT`, `DC`, `*IM`), nunca el comando entero ni la pantalla. La respuesta completa
378
402
  está disponible en `onRawResponse`.
379
403
 
404
+ ### Historial del PNR
405
+
406
+ `session.reservation.history(locator, options?)` lee el historial del PNR (Get Itinerary History,
407
+ `TravelItineraryHistoryLLSRQ` 2.4.0, el `*H` del host), una entrada por transacción:
408
+
409
+ ```ts
410
+ const { transactions } = await legacy.withSession((session) => session.reservation.history('ABC123'));
411
+ // transactions[0] → { rph, createdAt: '2026-11-04T17:15', agent, agentDutyCode, pcc, aaaPcc,
412
+ // receivedFrom, flightSegments, generalInfo, remarks, accounting }
413
+ ```
414
+
415
+ - **Quién, cuándo y desde dónde**: `createdAt` (con año, sin zona horaria), el sine y el duty code
416
+ del agente, el `pcc` de la firma, el `aaaPcc` del AAA y `receivedFrom`.
417
+ - **Qué cambió, con su `action`**: `flightSegments` con su estado en ese momento (`AS` alta, `XS`
418
+ baja; fechas sin año, como las manda el host), y SSR, ticketing, teléfonos, nombres y emails como
419
+ texto en `generalInfo` (`A4S`, `A7`, `A9`, `AN`, `APE`…). Sabre no publica la lista de valores de
420
+ `action`. Asientos, autos, hoteles, tren, seguros y facturas no se tipan: quedan en la respuesta
421
+ cruda (`onRawResponse`).
422
+ - **Trae datos del pasajero** (nombres, teléfonos, emails). El SDK nunca los manda al log.
423
+ - **No registra las colocaciones en cola**, ni las del sistema ni un `place` manual (CERT): para eso están
424
+ `queue.list`/`access` o los remarks `QUEUE_PLACE` de Get Booking.
425
+ - **Llámalo fuera de un recorrido de cola.** Dentro de una cola, Sabre rechaza el historial de otro
426
+ PNR con `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY`, un `SabreBusinessError` (CERT). El
427
+ PNR del AAA no cambia, `queue.state` sigue en `'inside'` y el recorrido sigue con `ignore`.
428
+ - Es una lectura: se reintenta. Un `locator` que no es de 6 caracteres alfanuméricos en mayúscula
429
+ lanza un `TypeError` sin llamar a la red. Un PNR que no existe lanza un `SabreBusinessError`
430
+ (`service: 'itineraryHistory'`).
431
+
380
432
  ### Colas
381
433
 
382
434
  Las operaciones de cola están en `session.queue`. `pcc` es opcional en cada llamada, y por defecto
383
- es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las colas alfabéticas
384
- (`QueueRef`). Las colas que devuelve `count` se pasan tal cual a las demás operaciones.
435
+ es el de la sesión. Una cola (`QueueRef`) tiene tres formas:
436
+
437
+ | Cola | `QueueRef` | Viaja como |
438
+ | - | - | - |
439
+ | Numérica, de 0 a 511 | `400` | `Number="400"` |
440
+ | Alfabética, una o dos letras mayúsculas | `'S'` | `Number="S"` |
441
+ | Con nombre (tablas QN y QNB) | `{ name: 'HOTELS' }` | `Name="HOTELS"` |
442
+
443
+ Las colas que devuelve `count` se pasan tal cual a las demás operaciones. `'S'` es la **cola de
444
+ mensajes** del PCC: tiene mensajes y no PNR, y Sabre no la lista, así que `list({ queue: 'S' })`
445
+ lanza un `TypeError` sin llamar a Sabre. Se recorre a mano: `access({ queue: 'S' })` devuelve el
446
+ primer mensaje (`kind: 'message'`) e `ignore()` el siguiente, sin sacarlo de la cola. `process` y
447
+ `place` dentro de la `S` lanzan un `TypeError` sin llamar a Sabre. `remove()` manda `QR`, pero
448
+ todavía no hay evidencia de qué hace Sabre con el mensaje.
449
+
450
+ > **Migración desde la 0.4.x**: `count` devolvía la cola `S` como `{ name: 'S' }`, que Sabre
451
+ > rechaza (`INVALID QUEUE NAME FORMAT`). Ahora devuelve `'S'`. Un código como
452
+ > `typeof q === 'number' ? … : q.name` deja de compilar, porque `q` puede ser un `string`. Un
453
+ > `{ name }` de una o dos letras mayúsculas ahora lanza un `TypeError` que pide pasar la letra, y
454
+ > `'12'` o `'s'` también lanzan: cada cola tiene una sola forma.
385
455
 
386
456
  | Método | Servicio de Sabre | Devuelve |
387
457
  | - | - | - |
388
458
  | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
389
- | `list({ queue, pcc?, primaryPassenger? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
459
+ | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
460
+ | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
390
461
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
391
462
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
392
463
  | `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
@@ -395,10 +466,30 @@ es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las co
395
466
 
396
467
  ```ts
397
468
  type QueueStep =
398
- | { done: false; item: QueueItem; warnings: SabreWarning[] }
469
+ | { done: false; kind: 'pnr'; item: QueueItem; warnings: SabreWarning[] }
470
+ | { done: false; kind: 'message'; message: { text: string[] }; warnings: SabreWarning[] } // cola 'S'
399
471
  | { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
400
472
  ```
401
473
 
474
+ > **Migración desde la 0.4.x**: un paso que no terminó ahora trae `kind`. Comprueba
475
+ > `step.kind === 'pnr'` antes de leer `step.item`. Solo la cola `S` devuelve `kind: 'message'`.
476
+
477
+ - **`list` devuelve la cola entera en una respuesta**: no pagina, ni con más de 1000 PNR (1221 en
478
+ unos 279 KB, en producción). `selection` la acota, pero en la lista Sabre solo acepta `QD`
479
+ (fecha de colocación en la cola, p. ej. `{ function: 'QD', criteria: '05NOV' }`) o `AK` (el
480
+ agente que lo colocó, PCC más firma, p. ej. `'PCC1AAT'`; todavía no se probó en la lista). Otro
481
+ código lanza `TypeError` sin llamar a Sabre. El `placedAt` de una entrada de la lista es cuándo
482
+ se colocó el PNR en la cola, la fecha por la que filtra `QD`.
483
+ - **`activity`** devuelve, por cola, los ocho contadores de la pantalla `QA` del host:
484
+ `numMessages` (los que están hoy en la cola), `ended`, `ignored`, `moved`, `removed`,
485
+ `transferred`, `leftMessageToContact` y `unableToReach`, más `totals` para el PCC. Sabre lista
486
+ también colas vacías con contadores. Ninguna fuente dice desde cuándo cuentan, así que el SDK los
487
+ devuelve tal cual. Como `count`, es una lectura: se reintenta, funciona dentro o fuera de una
488
+ cola y no pide duty code 9.
489
+ - **Cada resultado de colas trae `respondedAt`**: el `eb:Timestamp` de Sabre, en ISO 8601 UTC
490
+ (`'2026-11-06T17:34:16Z'`). A diferencia de `countedAt` y `placedAt`, que llegan como los manda
491
+ el host (sin año y en hora local del PCC), trae año y zona. Solo falta si Sabre no lo manda.
492
+
402
493
  - **El fin de cola es un resultado, no un error.** `END OF DISPLAY FOR REQUESTED DATA` da
403
494
  `{ done: true, reason: 'end-of-display' }`, y una cola vacía (`QUEUE SELECTED WAS EMPTY`) da
404
495
  `reason: 'empty'`. En los dos casos la sesión queda fuera de la cola. Cualquier otro error de
@@ -419,7 +510,9 @@ type QueueStep =
419
510
  0–191 o 200–254. Producción rechaza un `place` sin PIC, aunque el design lo dé como opcional.
420
511
  - **`QueueItem`** trae `locator`, el `placedAt` y el `placedBy` de `Line`, `primaryPassenger` y
421
512
  `group` cuando Sabre los manda, y `paragraph`: el PNR en formato de host, línea por línea y
422
- sin tocar. `placedAt` es el texto que llegó, con o sin año. `resolveHostDate` lo convierte en una
513
+ sin tocar. `placedAt` es el texto que llegó, con o sin año. Dentro de una cola es la última
514
+ modificación del PNR, no su colocación en la cola: esa es el `placedAt` de `list`, y para un
515
+ mismo PNR no coinciden. `resolveHostDate` lo convierte en una
423
516
  fecha completa.
424
517
  - `count` dentro de una cola no cuenta el PNR que está en pantalla.
425
518
 
@@ -461,6 +554,19 @@ segmentos con su estado, el campo de ticketing con una marca `issued` y la líne
461
554
  - No lee los plazos que las aerolíneas escriben como texto libre en remarks, SSR u OSI. Su formato
462
555
  lo pone cada aerolínea. El texto sigue en `paragraph`.
463
556
 
557
+ ### Leer un remark `QUEUE_PLACE`
558
+
559
+ Get Booking devuelve dónde se colocó el PNR como un remark con `type: 'QUEUE_PLACE'`. Su `text`
560
+ junta el PCC, la fecha, la cola y el PIC sin separadores. `parseQueuePlaceRemark(text)` lo separa:
561
+
562
+ ```ts
563
+ parseQueuePlaceRemark('MEX25SEP146/015'); // { pcc: 'MEX', date: '25SEP', queue: 146, pic: 15 }
564
+ ```
565
+
566
+ Devuelve `null` si el texto no tiene esa forma, o si el día, el mes, la cola (0–511) o el PIC
567
+ (0–191, 200–254) están fuera de rango. `date` viene sin año: se resuelve con
568
+ `resolveHostDate(date, referenceDate)`. Leer los remarks de la reserva le toca al consumidor.
569
+
464
570
  ### Probar tu lógica de colas: `sabre-client/testing`
465
571
 
466
572
  `sabre-client/testing` es una entrada aparte, con un fake de las sesiones y las colas de Sabre. No
@@ -492,16 +598,68 @@ fake.openSessions; // 0: la sesión se cerró
492
598
  `{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A un párrafo sin línea de
493
599
  firma se le agrega una sintética. `pnrs` agrega PNR que no están en ninguna cola, para
494
600
  `place({ locators })`. El fake no tiene AAA: fuera de una cola, `place` necesita `locators`.
601
+ - `queues['S']` es la cola de mensajes: cada elemento es el texto de un mensaje (`string[]`), no
602
+ un PNR. `access` lo muestra sin `Line`, como Sabre, y `fake.messages()` da los que quedan. Que
603
+ `remove` saque el mensaje es un supuesto del fake, no se probó con Sabre.
604
+ - `queue.activity`: `numMessages` sale de las colas del fake (sin el PNR en pantalla, como en
605
+ `count`, igual que CERT). Los otros siete contadores valen 0, salvo que los siembres con
606
+ `activity: { '9': { ignored: 27 } }`. Las acciones del fake **no** los cambian: nadie probó qué
607
+ contador sube Sabre con cada acción.
608
+ - `reservation.history`: `histories: { AAAAAA: [transacciones] }` fija lo que devuelve cada PNR,
609
+ con la forma de `ReservationHistoryTransaction` (las listas que falten llegan vacías). Un PNR que
610
+ el fake conoce sin historial sembrado devuelve una transacción sintética (su creación), y un
611
+ localizador desconocido da un error de host marcado como no simulado. Dentro de una cola, el
612
+ historial de otro PNR se rechaza con `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY` y el del
613
+ PNR en pantalla se devuelve, como en CERT.
495
614
  - Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
496
615
  obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
497
616
  SDK.
498
617
  - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni `selection`, que recibe
499
- un error en vez de la cola entera.
618
+ un error en vez de la cola entera, en `access` y en `list`.
500
619
  - `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
501
- `emptyQueue`, `place`…), como `{ operation, status, body }`.
620
+ `emptyQueue`, `place`…), como `{ operation, status, body }`. Cada una tiene sus propios alias de
621
+ localizador (`walk`: `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`,
622
+ `placeWithoutInstructionCodeInside`: `AAAAAG`–`AAAAAH`), y sus líneas de firma conservan la
623
+ forma (`… <alias> H`).
624
+ - `queueFakePnrs(fixture)` convierte una grabación en los PNR del fake: primero los de `list`, en
625
+ orden, con su `Line`, y el párrafo que mostró `access`, `ignore`, `remove` o `place` para cada
626
+ uno. La cola la eliges tú, y puedes juntar fixtures:
627
+
628
+ ```ts
629
+ const fake = createQueueFake({
630
+ queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
631
+ pnrs: queueFakePnrs(queueFixtures.place),
632
+ });
633
+ ```
502
634
 
503
635
  `examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
504
636
 
637
+ #### Anonimizar respuestas grabadas
638
+
639
+ `createAnonymizer` aplica la regla con la que se grabaron las fixtures del SDK, para guardar las
640
+ respuestas de Sabre (por ejemplo, las de `onRawResponse`) sin datos de pasajeros:
641
+
642
+ ```ts
643
+ import { createAnonymizer } from 'sabre-client/testing';
644
+
645
+ const anonymizer = createAnonymizer({ secrets: [pcc, username] });
646
+ const safe = anonymizer.anonymize(raw.body); // un sobre SOAP entero
647
+ anonymizer.maskParagraph(item.paragraph); // líneas en formato de host
648
+ if (anonymizer.leaks(safe).length > 0) throw new Error('quedan datos reales');
649
+ ```
650
+
651
+ - Cada localizador recibe un alias (`AAAAAA`, `AAAAAB`…), el mismo en todos los lugares donde
652
+ aparece, también en los textos que pasen después por el mismo anonimizador. `PassengerName` pasa
653
+ a `PASAJERO/UNO`, `Group` a `GRUPO`, `AgentSine` a `AAA` (salvo `SABRE`), `PseudoCityCode` a
654
+ `XXXX`, `LNIATA` a `XXXXXX` y cada `secret` a `XXXX`.
655
+ - En las líneas en formato de host (`<Text>`, o `maskLine` y `maskParagraph`), solo quedan tal cual
656
+ las de una lista blanca (`isSafeHostLine`: líneas vacías, avisos PIC como `011 SEE REMARKS`,
657
+ títulos de sección, el fin de cola). En las demás, las letras pasan a `X` y los dígitos a `9`. La
658
+ línea de firma conserva su forma, y su localizador solo si ya es un alias.
659
+ - `leaks(text)` lista los datos reales que siguen en `text` (`'secreto'`, `'localizador'`,
660
+ `'nombre'`, `'línea del PNR'`, `'token'`): úsalo antes de escribir un archivo.
661
+ - Usa un anonimizador por grabación. Es una regla, no una garantía: revisa lo que guardas.
662
+
505
663
  ## Manejo de errores
506
664
 
507
665
  Todos los errores que lanza el SDK extienden `SabreError`:
@@ -572,6 +730,39 @@ En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `o
572
730
  > `body` es la respuesta cruda y **trae datos de pasajeros**. El SDK nunca lo manda al logger.
573
731
  > Dónde lo guardas es tu responsabilidad.
574
732
 
733
+ ## Request crudo
734
+
735
+ Las mismas operaciones aceptan `onRawRequest`, al lado de `onRawResponse`. Recibe lo que el SDK
736
+ mandó por la red, por ejemplo para mostrar el request real junto a la respuesta:
737
+
738
+ ```ts
739
+ import type { SabreRawRequest } from 'sabre-client';
740
+
741
+ let sent: SabreRawRequest | undefined;
742
+ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) });
743
+ // sent: { operation: 'booking.get', protocol: 'rest', method, url, headers, body, attempts }
744
+ ```
745
+
746
+ - `operation` es el mismo valor que recibe `onRawResponse`. `url` es la URL completa, con su query
747
+ string. `headers` son los que se mandaron, **sin `Authorization`** (se quita el header entero).
748
+ `body` es el texto que se mandó, o `undefined` si no hubo. En REST va tal cual; en
749
+ `SabreLegacy` es el sobre SOAP completo con el `BinarySecurityToken` reemplazado por
750
+ `[REDACTED]`, igual que en la respuesta.
751
+ - Se llama **una vez**, con el último intento que se mandó, antes de que la operación devuelva o
752
+ lance y antes que `onRawResponse`. A diferencia de este, se llama también si ningún intento tuvo
753
+ respuesta HTTP (error de red, abort), siempre que se haya mandado al menos uno. No se llama si la
754
+ operación lanza antes de mandar nada (por ejemplo, un `TypeError` de validación).
755
+ - En SOAP, `eb:MessageId` y `eb:Timestamp` son nuevos en cada intento, así que el sobre que llega
756
+ es el del último intento.
757
+ - No se llama con la autenticación: el request de token de OAuth, `openSession`
758
+ (`SessionCreateRQ`, cuyo sobre lleva la contraseña del EPR) y `close` (`SessionCloseRQ`).
759
+ - Es síncrono y su retorno se ignora. Si lanza, el SDK registra
760
+ `sabre.request.rawRequestHookFailed` sin el body y la operación sigue.
761
+
762
+ > [!WARNING]
763
+ > `body` y `url` **traen datos de pasajeros** (localizadores, nombres, contactos). El SDK nunca los
764
+ > manda al logger. Dónde los guardas es tu responsabilidad.
765
+
575
766
  ## Reintentos
576
767
 
577
768
  Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
@@ -627,7 +818,8 @@ const sabre = new Sabre({
627
818
  ```
628
819
 
629
820
  El SDK registra eventos estructurados (`sabre.request.start`, `sabre.request.end`,
630
- `sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
821
+ `sabre.request.retry`, `sabre.request.rawResponseHookFailed`,
822
+ `sabre.request.rawRequestHookFailed`, `sabre.auth.token.refresh`,
631
823
  `sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
632
824
  `sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
633
825
  `sabre.session.command`, `sabre.queue.exitTolerated`, `sabre.queue.exitFailed`) y nunca los bodies
package/README.md CHANGED
@@ -12,17 +12,31 @@ TypeScript SDK for the Sabre APIs.
12
12
 
13
13
  ## Status
14
14
 
15
- `0.4.0` is the current release of the rewrite. It covers Tier 1 and Tier 2 of the Air Booking
16
- flow over REST, and Tier 3 (SOAP sessions and queues). It adds `buildCredentials` and host
17
- commands (`session.command`) to `0.3.0`, with no breaking changes.
15
+ `0.5.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.4.1`.
16
+ It covers Tier 1 and Tier 2 of the Air Booking flow over REST, Tier 3 (SOAP sessions and queues)
17
+ and Tier 4: queue activity, PNR history, the `S` message queue, `selection` in `list` and
18
+ `onRawRequest`. It has three breaking changes (see [Upgrading from 0.4](#upgrading-from-04)).
18
19
 
19
20
  ```sh
20
- npm install sabre-client
21
+ npm install sabre-client@next # 0.5.0
22
+ npm install sabre-client # 0.4.1
21
23
  ```
22
24
 
23
25
  The package is still `0.x`: a minor version may include breaking changes, and the
24
26
  [CHANGELOG](./CHANGELOG.md) marks them.
25
27
 
28
+ ### Upgrading from 0.4
29
+
30
+ - **`pcc` is no longer in the config.** Passing it to `new Sabre` or `new SabreLegacy` is a type
31
+ error, and in JavaScript it is ignored. The PCC you authenticate with goes in `credentials`
32
+ (see [Credentials](#credentials)); another PCC goes per operation, as `targetPcc` in REST or
33
+ `pcc` in the queue operations.
34
+ - **`QueueRef` can be a `string`.** An alphabetic queue like `'S'` is `'S'`, and `count` and
35
+ `place` return it that way instead of `{ name: 'S' }`. Code that does
36
+ `typeof q === 'number' ? … : q.name` must handle the `string` case.
37
+ - **A `QueueStep` that is not done has a `kind`.** Check `step.kind === 'pnr'` before reading
38
+ `step.item`; `kind: 'message'` carries a message of the `S` queue.
39
+
26
40
  Tier 1 covers the core Air Booking flow over REST:
27
41
 
28
42
  | Step | Service | Method |
@@ -45,11 +59,12 @@ Tier 3 adds the `SabreLegacy` SOAP client, with a stateful session and Queue Man
45
59
  | Step | Service | Method |
46
60
  | - | - | - |
47
61
  | Session | `SessionCreateRQ` 2.0.0 / 1.0.0, `OTA_PingRQ`, `SessionCloseRQ` | `legacy.withSession`, `legacy.openSession` |
48
- | Queues | Get Queue Count, Access Queue, Place Queue Message | `session.queue.count`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
49
- | Queue paragraph | — (pure function) | `parseQueueParagraph`, `resolveHostDate` |
62
+ | Queues | Get Queue Count, Get Queue Activity, Access Queue, Place Queue Message | `session.queue.count`, `activity`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
63
+ | Queue paragraph | — (pure function) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
64
+ | PNR history | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
50
65
  | Host commands | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
51
66
 
52
- Planned after `0.4.0`: Get Ancillaries (NDC only).
67
+ Planned after `0.5.0`: Get Ancillaries (NDC only).
53
68
 
54
69
  ## Requirements
55
70
 
@@ -115,6 +130,14 @@ const sabre = new Sabre({ environment: 'sandbox', credentials });
115
130
  const legacy = new SabreLegacy({ environment: 'sandbox', credentials });
116
131
  ```
117
132
 
133
+ The object that `buildCredentials` returns carries its `authVersion`, so the config does not
134
+ need to repeat it: with `authVersion: 'v2'` in `buildCredentials`, `{ environment, credentials }`
135
+ is already a v2 config. If you pass `authVersion` in the config too, it must match, or the client
136
+ throws a `TypeError`. Credentials you build by hand have no `authVersion`, so with v2 the config
137
+ still needs `authVersion: 'v2'`. If the version is only known at runtime (for example, read
138
+ from an environment variable into a `BuildCredentialsOptions`), the result of `buildCredentials`
139
+ can still go straight into the config, with no `authVersion` there.
140
+
118
141
  The PCC in the credentials is the one you authenticate with. Working in another PCC is not a
119
142
  second set of credentials: pass it per operation, as `targetPcc` in `booking.create`, `get` and
120
143
  `cancel`, or as `pcc` in the queue operations.
@@ -349,6 +372,7 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
349
372
  // screen.text → 'PSGR TYPE - ON' (as Sabre sends it, not trimmed)
350
373
  // screen.warnings → the stl:Warning entries, if any
351
374
  // screen.messageId → eb:MessageId, for Sabre support
375
+ // screen.respondedAt → eb:Timestamp in UTC, e.g. '2026-11-06T17:34:16Z'
352
376
  ```
353
377
 
354
378
  - **The command is sent as is.** In this service Sabre's `‡` separator is written `¥`
@@ -373,16 +397,63 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
373
397
  characters (`W/PT`, `DC`, `*IM`), never the whole command or the screen. The full response is
374
398
  available through `onRawResponse`.
375
399
 
400
+ ### PNR history
401
+
402
+ `session.reservation.history(locator, options?)` reads the PNR history (Get Itinerary History,
403
+ `TravelItineraryHistoryLLSRQ` 2.4.0, the host's `*H`), one entry per transaction:
404
+
405
+ ```ts
406
+ const { transactions } = await legacy.withSession((session) => session.reservation.history('ABC123'));
407
+ // transactions[0] → { rph, createdAt: '2026-11-04T17:15', agent, agentDutyCode, pcc, aaaPcc,
408
+ // receivedFrom, flightSegments, generalInfo, remarks, accounting }
409
+ ```
410
+
411
+ - **Who, when and from where**: `createdAt` (with a year, no time zone), the agent's sine and duty
412
+ code, the signing `pcc`, the AAA's `aaaPcc` and `receivedFrom`.
413
+ - **What changed, with its `action`**: `flightSegments` with their status at that moment
414
+ (`AS` added, `XS` cancelled; dates without a year, as the host sends them), and SSR, ticketing,
415
+ phones, names and emails as text in `generalInfo` (`A4S`, `A7`, `A9`, `AN`, `APE`…). Sabre does
416
+ not publish the list of `action` values. Seats, cars, hotels, rail, insurance and invoices are
417
+ not typed: they stay in the raw response (`onRawResponse`).
418
+ - **It carries passenger data** (names, phones, emails). The SDK never logs it.
419
+ - **It does not record queue placements**, neither the system's nor a manual `place` (CERT): for that, use
420
+ `queue.list`/`access` or the `QUEUE_PLACE` remarks of Get Booking.
421
+ - **Call it outside a queue walk.** Inside a queue, Sabre rejects the history of another PNR with
422
+ `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY`, a `SabreBusinessError` (CERT). The PNR in
423
+ the AAA does not change, `queue.state` stays `'inside'`, and the walk goes on with `ignore`.
424
+ - It is a read: it is retried. A `locator` that is not 6 uppercase alphanumeric characters throws a
425
+ `TypeError` without calling the network. A PNR that does not exist throws a `SabreBusinessError`
426
+ (`service: 'itineraryHistory'`).
427
+
376
428
  ### Queues
377
429
 
378
430
  The queue operations live in `session.queue`. `pcc` is optional in every call and defaults to the
379
- session's PCC. A queue is a number from 0 to 511 or `{ name }` for alphabetic queues
380
- (`QueueRef`). The queues returned by `count` can be passed straight to the other operations.
431
+ session's PCC. A queue (`QueueRef`) comes in three forms:
432
+
433
+ | Queue | `QueueRef` | Sent as |
434
+ | - | - | - |
435
+ | Numeric, 0 to 511 | `400` | `Number="400"` |
436
+ | Alphabetic, one or two uppercase letters | `'S'` | `Number="S"` |
437
+ | Named (QN and QNB tables) | `{ name: 'HOTELS' }` | `Name="HOTELS"` |
438
+
439
+ The queues returned by `count` can be passed straight to the other operations. `'S'` is the PCC's
440
+ **message queue**: it holds messages, not PNRs, and Sabre does not list it, so `list({ queue: 'S' })`
441
+ throws a `TypeError` without calling Sabre. Walk it by hand: `access({ queue: 'S' })` returns the
442
+ first message (`kind: 'message'`) and `ignore()` the next one, leaving it in the queue. `process`
443
+ and `place` inside `S` throw a `TypeError` without calling Sabre. `remove()` sends `QR`, but there
444
+ is no evidence yet of what Sabre does with the message.
445
+
446
+ > **Migrating from 0.4.x**: `count` used to return the `S` queue as `{ name: 'S' }`, which Sabre
447
+ > rejects (`INVALID QUEUE NAME FORMAT`). It now returns `'S'`. Code that does
448
+ > `typeof q === 'number' ? … : q.name` no longer compiles, because `q` can be a `string`. A
449
+ > `{ name }` of one or two uppercase letters now throws a `TypeError` that tells you to pass the
450
+ > letter instead, and `'12'` or `'s'` throw too: each queue has a single form.
381
451
 
382
452
  | Method | Sabre service | Returns |
383
453
  | - | - | - |
384
454
  | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
385
- | `list({ queue, pcc?, primaryPassenger? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
455
+ | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
456
+ | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
386
457
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
387
458
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
388
459
  | `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
@@ -391,10 +462,30 @@ session's PCC. A queue is a number from 0 to 511 or `{ name }` for alphabetic qu
391
462
 
392
463
  ```ts
393
464
  type QueueStep =
394
- | { done: false; item: QueueItem; warnings: SabreWarning[] }
465
+ | { done: false; kind: 'pnr'; item: QueueItem; warnings: SabreWarning[] }
466
+ | { done: false; kind: 'message'; message: { text: string[] }; warnings: SabreWarning[] } // queue 'S'
395
467
  | { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
396
468
  ```
397
469
 
470
+ > **Migrating from 0.4.x**: a step that is not done now has a `kind`. Check `step.kind === 'pnr'`
471
+ > before reading `step.item`. Only the `S` queue returns `kind: 'message'`.
472
+
473
+ - **`list` returns the whole queue in one response**: there is no paging, even with more than 1000
474
+ PNRs (1221 in about 279 KB, in production). `selection` narrows it, but in the list Sabre only
475
+ accepts `QD` (date placed on the queue, e.g. `{ function: 'QD', criteria: '05NOV' }`) or `AK`
476
+ (the agent who placed it, PCC plus sine, e.g. `'PCC1AAT'`; not tested in the list yet).
477
+ Any other code throws a `TypeError` without calling Sabre. The `placedAt` of a list entry is
478
+ when the PNR was placed on the queue, the date `QD` filters on.
479
+ - **`activity`** returns, per queue, the eight counters of the host's `QA` screen: `numMessages`
480
+ (on the queue now), `ended`, `ignored`, `moved`, `removed`, `transferred`,
481
+ `leftMessageToContact` and `unableToReach`, plus `totals` for the PCC. Sabre also lists empty
482
+ queues that have counters. No source says since when the counters run, so the SDK returns them
483
+ as they come. Like `count`, it is a read: it is retried, it works inside or outside a queue, and
484
+ it does not need duty code 9.
485
+ - **Every queue result carries `respondedAt`**: Sabre's `eb:Timestamp`, in ISO 8601 UTC
486
+ (`'2026-11-06T17:34:16Z'`). Unlike `countedAt` and `placedAt`, which come as the host sends them
487
+ (no year, PCC local time), it has a year and a zone. It is missing only if Sabre does not send it.
488
+
398
489
  - **The end of the queue is a result, not an error.** `END OF DISPLAY FOR REQUESTED DATA` gives
399
490
  `{ done: true, reason: 'end-of-display' }`, and an empty queue (`QUEUE SELECTED WAS EMPTY`)
400
491
  gives `reason: 'empty'`. In both cases the session is left outside the queue. Any other Sabre
@@ -414,7 +505,9 @@ type QueueStep =
414
505
  Production rejects a `place` without it, although the design says it is optional.
415
506
  - **`QueueItem`** has `locator`, the `placedAt` and `placedBy` of `Line`, `primaryPassenger` and
416
507
  `group` when Sabre sends them, and `paragraph`: the PNR in host format, line by line and
417
- untouched. `placedAt` is the text that arrived, with or without the year. `resolveHostDate`
508
+ untouched. `placedAt` is the text that arrived, with or without the year. Inside a queue it is
509
+ the PNR's last change, not when it was placed on the queue: that one is the `placedAt` of
510
+ `list`, and the two differ for the same PNR. `resolveHostDate`
418
511
  turns it into a full date.
419
512
  - `count` inside a queue does not count the PNR on screen.
420
513
 
@@ -455,6 +548,20 @@ segments with their status, the ticketing field with an `issued` flag, and the r
455
548
  - It does not read deadlines that airlines write as free text in remarks, SSR or OSI. Their format
456
549
  belongs to each airline. The text is still in `paragraph`.
457
550
 
551
+ ### Reading a `QUEUE_PLACE` remark
552
+
553
+ Get Booking returns where the PNR was placed as a remark with `type: 'QUEUE_PLACE'`. Its `text`
554
+ packs the PCC, the date, the queue and the PIC with no separators. `parseQueuePlaceRemark(text)`
555
+ splits it:
556
+
557
+ ```ts
558
+ parseQueuePlaceRemark('MEX25SEP146/015'); // { pcc: 'MEX', date: '25SEP', queue: 146, pic: 15 }
559
+ ```
560
+
561
+ It returns `null` if the text does not have that shape, or if the day, month, queue (0–511) or PIC
562
+ (0–191, 200–254) are out of range. `date` has no year: resolve it with
563
+ `resolveHostDate(date, referenceDate)`. Reading the remarks from the booking is up to you.
564
+
458
565
  ### Testing your queue logic: `sabre-client/testing`
459
566
 
460
567
  `sabre-client/testing` is a separate entry point with a fake of Sabre's sessions and queues. It is
@@ -485,16 +592,67 @@ fake.openSessions; // 0: the session was closed
485
592
  `{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A paragraph without a
486
593
  signature line gets a synthetic one. `pnrs` adds PNRs that are not in any queue, for
487
594
  `place({ locators })`. The fake has no AAA: outside a queue, `place` needs `locators`.
595
+ - `queues['S']` is the message queue: each item is a message's text (`string[]`), not a PNR.
596
+ `access` shows it without `Line`, as Sabre does, and `fake.messages()` lists the ones left.
597
+ That `remove` takes a message out is an assumption of the fake, not tested against Sabre.
598
+ - `queue.activity`: `numMessages` comes from the fake's queues (without the PNR on screen, as in
599
+ `count`, as CERT does). The other seven counters are 0 unless you seed them with
600
+ `activity: { '9': { ignored: 27 } }`. The fake's actions do **not** change them: no one has
601
+ tested which counter Sabre raises for each action.
602
+ - `reservation.history`: `histories: { AAAAAA: [transactions] }` sets what each PNR returns, in
603
+ the shape of `ReservationHistoryTransaction` (missing lists come back empty). A PNR the fake
604
+ knows without a seeded history returns one synthetic transaction (its creation), and an unknown
605
+ locator gives a host error marked as not simulated. Inside a queue, the history of another PNR
606
+ is rejected with `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY` and the one on screen is
607
+ returned, as in CERT.
488
608
  - It answers like production: empty queues, the end of the queue, `place` moving the PNR, the
489
609
  required PIC, and an expired session with `fake.expireSessions()`. `fake.calls` lists what the
490
610
  SDK asked for.
491
611
  - **Not simulated**: PCCs (the queues are the same for any `pcc`) and `selection`, which gets an
492
- error instead of the whole queue.
612
+ error instead of the whole queue, in `access` and in `list`.
493
613
  - `queueFixtures` holds the anonymized responses recorded in production (`walk`, `emptyQueue`,
494
- `place`, …), as `{ operation, status, body }`.
614
+ `place`, …), as `{ operation, status, body }`. Each one has its own locator aliases (`walk`:
615
+ `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`, `placeWithoutInstructionCodeInside`:
616
+ `AAAAAG`–`AAAAAH`), and their signature lines keep their shape (`… <alias> H`).
617
+ - `queueFakePnrs(fixture)` turns a recording into the fake's PNRs: those of `list` first, in
618
+ order, with their `Line`, and the paragraph `access`, `ignore`, `remove` or `place` showed for
619
+ each one. You choose the queue, and you can mix fixtures:
620
+
621
+ ```ts
622
+ const fake = createQueueFake({
623
+ queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
624
+ pnrs: queueFakePnrs(queueFixtures.place),
625
+ });
626
+ ```
495
627
 
496
628
  `examples/queue-review.mjs` is a full example, and its test runs it against the fake.
497
629
 
630
+ #### Anonymizing recorded responses
631
+
632
+ `createAnonymizer` applies the rule the SDK's fixtures were recorded with, so you can store
633
+ Sabre's responses (for example from `onRawResponse`) without passenger data:
634
+
635
+ ```ts
636
+ import { createAnonymizer } from 'sabre-client/testing';
637
+
638
+ const anonymizer = createAnonymizer({ secrets: [pcc, username] });
639
+ const safe = anonymizer.anonymize(raw.body); // a whole SOAP envelope
640
+ anonymizer.maskParagraph(item.paragraph); // host-format lines
641
+ if (anonymizer.leaks(safe).length > 0) throw new Error('real data left');
642
+ ```
643
+
644
+ - Each locator gets an alias (`AAAAAA`, `AAAAAB`…), the same one everywhere it appears, also in
645
+ later texts passed to the same anonymizer. `PassengerName` becomes `PASAJERO/UNO`, `Group`
646
+ becomes `GRUPO`, `AgentSine` becomes `AAA` (except `SABRE`), `PseudoCityCode` becomes `XXXX`,
647
+ `LNIATA` becomes `XXXXXX`, and each `secret` becomes `XXXX`.
648
+ - In host-format lines (`<Text>`, or `maskLine` and `maskParagraph`), only the lines of a
649
+ whitelist stay as they are (`isSafeHostLine`: empty lines, PIC notices like `011 SEE REMARKS`,
650
+ section titles, the end of the queue). In the others, letters become `X` and digits `9`. The
651
+ signature line keeps its shape, and its locator only if it is already an alias.
652
+ - `leaks(text)` lists the real data still in `text` (`'secreto'`, `'localizador'`, `'nombre'`,
653
+ `'línea del PNR'`, `'token'`): use it before writing a file.
654
+ - Use one anonymizer per recording. It is a rule, not a guarantee: check what you store.
655
+
498
656
  ## Error handling
499
657
 
500
658
  Every error thrown by the SDK extends `SabreError`:
@@ -565,6 +723,39 @@ In `SabreLegacy`, `protocol` is `'soap'`, `body` is the whole SOAP envelope, and
565
723
  > `body` is the raw response and **contains passenger data**. The SDK never sends it to the
566
724
  > logger. Where you store it is up to you.
567
725
 
726
+ ## Raw request
727
+
728
+ The same operations accept `onRawRequest`, next to `onRawResponse`. It receives what the SDK sent
729
+ over the network, for example to show the real request next to the response:
730
+
731
+ ```ts
732
+ import type { SabreRawRequest } from 'sabre-client';
733
+
734
+ let sent: SabreRawRequest | undefined;
735
+ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) });
736
+ // sent: { operation: 'booking.get', protocol: 'rest', method, url, headers, body, attempts }
737
+ ```
738
+
739
+ - `operation` is the same value `onRawResponse` gets. `url` is the full URL, with its query
740
+ string. `headers` are the ones sent, **without `Authorization`** (the whole header is dropped).
741
+ `body` is the text sent, or `undefined` if there was none. In REST it is untouched; in
742
+ `SabreLegacy` it is the whole SOAP envelope with the `BinarySecurityToken` replaced by
743
+ `[REDACTED]`, as in the response.
744
+ - It is called **once**, with the last attempt sent, before the operation returns or throws and
745
+ before `onRawResponse`. Unlike `onRawResponse`, it is also called when no attempt got an HTTP
746
+ response (network error, abort), as long as at least one was sent. It is not called if the
747
+ operation throws before sending anything (for example a `TypeError` from validation).
748
+ - In SOAP, `eb:MessageId` and `eb:Timestamp` are new on every attempt, so the envelope you get is
749
+ the one of the last attempt.
750
+ - It is not called for authentication: the OAuth token request, `openSession`
751
+ (`SessionCreateRQ`, whose envelope carries the EPR password) and `close` (`SessionCloseRQ`).
752
+ - It is synchronous and its return value is ignored. If it throws, the SDK logs
753
+ `sabre.request.rawRequestHookFailed` without the body, and the operation carries on.
754
+
755
+ > [!WARNING]
756
+ > `body` and `url` **contain passenger data** (locators, names, contact details). The SDK never
757
+ > sends them to the logger. Where you store them is up to you.
758
+
568
759
  ## Retries
569
760
 
570
761
  Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
@@ -621,7 +812,8 @@ const sabre = new Sabre({
621
812
  ```
622
813
 
623
814
  The SDK logs structured events (`sabre.request.start`, `sabre.request.end`,
624
- `sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
815
+ `sabre.request.retry`, `sabre.request.rawResponseHookFailed`,
816
+ `sabre.request.rawRequestHookFailed`, `sabre.auth.token.refresh`,
625
817
  `sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
626
818
  `sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
627
819
  `sabre.session.command`, `sabre.queue.exitTolerated`, `sabre.queue.exitFailed`), never raw