sabre-client 0.4.1 → 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
 
@@ -88,11 +103,6 @@ const sabre = new Sabre({
88
103
  | `fetch` | `fetch` global | `fetch` propio (proxies, tests, otros runtimes) |
89
104
  | `tokenStore` | solo en memoria | Comparte el token entre procesos (ver [Reutilizar el token](#reutilizar-el-token)) |
90
105
 
91
- > **`pcc` está deprecado** desde la `0.4.1` y se quita en la `0.5.0`. Era un campo reservado, y
92
- > ningún cliente lo lee. El PCC con el que te autenticas va en `credentials` (ver
93
- > [Credenciales](#credenciales) y `buildCredentials`). Para trabajar en otro PCC, pásalo en cada
94
- > operación: `targetPcc` en REST y `pcc` en las operaciones de colas.
95
-
96
106
  ### Credenciales
97
107
 
98
108
  Hay **un solo juego de credenciales** para REST y SOAP: el mismo objeto `credentials` sirve para
@@ -124,7 +134,9 @@ El objeto que devuelve `buildCredentials` lleva su `authVersion`, así que la co
124
134
  repetirlo: con `authVersion: 'v2'` en `buildCredentials`, `{ environment, credentials }` ya es una
125
135
  config v2. Si también pasas `authVersion` en la config, tiene que coincidir; si no, el cliente lanza
126
136
  un `TypeError`. Las credenciales que armas a mano no llevan `authVersion`, así que con v2 la config
127
- sigue necesitando `authVersion: 'v2'`.
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.
128
140
 
129
141
  El PCC de las credenciales es con el que te autenticas. Trabajar en otro PCC no requiere otro
130
142
  juego de credenciales: se pasa en cada operación, como `targetPcc` en `booking.create`, `get` y
@@ -361,6 +373,7 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
361
373
  // screen.text → 'PSGR TYPE - ON' (como lo manda Sabre, sin recortar)
362
374
  // screen.warnings → los stl:Warning, si los hay
363
375
  // screen.messageId → eb:MessageId, para soporte de Sabre
376
+ // screen.respondedAt → eb:Timestamp en UTC, p. ej. '2026-11-06T17:34:16Z'
364
377
  ```
365
378
 
366
379
  - **El comando va tal cual.** En este servicio, el separador `‡` de Sabre se escribe `¥`
@@ -388,16 +401,63 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
388
401
  caracteres (`W/PT`, `DC`, `*IM`), nunca el comando entero ni la pantalla. La respuesta completa
389
402
  está disponible en `onRawResponse`.
390
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
+
391
432
  ### Colas
392
433
 
393
434
  Las operaciones de cola están en `session.queue`. `pcc` es opcional en cada llamada, y por defecto
394
- es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las colas alfabéticas
395
- (`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.
396
455
 
397
456
  | Método | Servicio de Sabre | Devuelve |
398
457
  | - | - | - |
399
458
  | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
400
- | `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 |
401
461
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
402
462
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
403
463
  | `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
@@ -406,10 +466,30 @@ es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las co
406
466
 
407
467
  ```ts
408
468
  type QueueStep =
409
- | { 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'
410
471
  | { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
411
472
  ```
412
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
+
413
493
  - **El fin de cola es un resultado, no un error.** `END OF DISPLAY FOR REQUESTED DATA` da
414
494
  `{ done: true, reason: 'end-of-display' }`, y una cola vacía (`QUEUE SELECTED WAS EMPTY`) da
415
495
  `reason: 'empty'`. En los dos casos la sesión queda fuera de la cola. Cualquier otro error de
@@ -430,7 +510,9 @@ type QueueStep =
430
510
  0–191 o 200–254. Producción rechaza un `place` sin PIC, aunque el design lo dé como opcional.
431
511
  - **`QueueItem`** trae `locator`, el `placedAt` y el `placedBy` de `Line`, `primaryPassenger` y
432
512
  `group` cuando Sabre los manda, y `paragraph`: el PNR en formato de host, línea por línea y
433
- 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
434
516
  fecha completa.
435
517
  - `count` dentro de una cola no cuenta el PNR que está en pantalla.
436
518
 
@@ -472,6 +554,19 @@ segmentos con su estado, el campo de ticketing con una marca `issued` y la líne
472
554
  - No lee los plazos que las aerolíneas escriben como texto libre en remarks, SSR u OSI. Su formato
473
555
  lo pone cada aerolínea. El texto sigue en `paragraph`.
474
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
+
475
570
  ### Probar tu lógica de colas: `sabre-client/testing`
476
571
 
477
572
  `sabre-client/testing` es una entrada aparte, con un fake de las sesiones y las colas de Sabre. No
@@ -503,16 +598,68 @@ fake.openSessions; // 0: la sesión se cerró
503
598
  `{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A un párrafo sin línea de
504
599
  firma se le agrega una sintética. `pnrs` agrega PNR que no están en ninguna cola, para
505
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.
506
614
  - Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
507
615
  obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
508
616
  SDK.
509
617
  - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni `selection`, que recibe
510
- un error en vez de la cola entera.
618
+ un error en vez de la cola entera, en `access` y en `list`.
511
619
  - `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
512
- `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
+ ```
513
634
 
514
635
  `examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
515
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
+
516
663
  ## Manejo de errores
517
664
 
518
665
  Todos los errores que lanza el SDK extienden `SabreError`:
@@ -583,6 +730,39 @@ En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `o
583
730
  > `body` es la respuesta cruda y **trae datos de pasajeros**. El SDK nunca lo manda al logger.
584
731
  > Dónde lo guardas es tu responsabilidad.
585
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
+
586
766
  ## Reintentos
587
767
 
588
768
  Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
@@ -638,7 +818,8 @@ const sabre = new Sabre({
638
818
  ```
639
819
 
640
820
  El SDK registra eventos estructurados (`sabre.request.start`, `sabre.request.end`,
641
- `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`,
642
823
  `sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
643
824
  `sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
644
825
  `sabre.session.command`, `sabre.queue.exitTolerated`, `sabre.queue.exitFailed`) y nunca los bodies