sabre-client 0.4.1 → 0.5.1

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,32 @@ 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.1` 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`. La línea `0.5` trae tres cambios incompatibles (ver
19
+ [Actualizar desde la 0.4](#actualizar-desde-la-04)).
18
20
 
19
21
  ```sh
20
- npm install sabre-client
22
+ npm install sabre-client@next # 0.5.1
23
+ npm install sabre-client # 0.4.1
21
24
  ```
22
25
 
23
26
  El paquete sigue en `0.x`: una versión minor puede traer cambios incompatibles, y el
24
27
  [CHANGELOG](./CHANGELOG.md) los marca.
25
28
 
29
+ ### Actualizar desde la 0.4
30
+
31
+ - **`pcc` ya no está en la config.** Pasarlo a `new Sabre` o a `new SabreLegacy` da un error de
32
+ tipos, y en JavaScript se ignora. El PCC con el que te autenticas va en `credentials` (ver
33
+ [Credenciales](#credenciales)); otro PCC se pasa en cada operación, como `targetPcc` en REST o
34
+ `pcc` en las operaciones de colas.
35
+ - **`QueueRef` puede ser un `string`.** Una cola alfabética como `'S'` es `'S'`, y `count` y
36
+ `place` la devuelven así en lugar de `{ name: 'S' }`. El código que hace
37
+ `typeof q === 'number' ? … : q.name` tiene que manejar el caso `string`.
38
+ - **Un `QueueStep` que no terminó tiene `kind`.** Revisa `step.kind === 'pnr'` antes de leer
39
+ `step.item`; `kind: 'message'` trae un mensaje de la cola `S`.
40
+
26
41
  El Tier 1 cubre el flujo básico de Air Booking por REST:
27
42
 
28
43
  | Paso | Servicio | Método |
@@ -45,11 +60,12 @@ El Tier 3 agrega el cliente SOAP `SabreLegacy`, con una sesión stateful y Queue
45
60
  | Paso | Servicio | Método |
46
61
  | - | - | - |
47
62
  | 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` |
63
+ | Colas | Get Queue Count, Get Queue Activity, Access Queue, Place Queue Message | `session.queue.count`, `activity`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
64
+ | Párrafo de cola | — (función pura) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
65
+ | Historial del PNR | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
50
66
  | Comandos de host | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
51
67
 
52
- Planeado después de `0.4.0`: Get Ancillaries (solo NDC).
68
+ Planeado después de `0.5.0`: Get Ancillaries (solo NDC).
53
69
 
54
70
  ## Requisitos
55
71
 
@@ -88,11 +104,6 @@ const sabre = new Sabre({
88
104
  | `fetch` | `fetch` global | `fetch` propio (proxies, tests, otros runtimes) |
89
105
  | `tokenStore` | solo en memoria | Comparte el token entre procesos (ver [Reutilizar el token](#reutilizar-el-token)) |
90
106
 
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
107
  ### Credenciales
97
108
 
98
109
  Hay **un solo juego de credenciales** para REST y SOAP: el mismo objeto `credentials` sirve para
@@ -124,7 +135,9 @@ El objeto que devuelve `buildCredentials` lleva su `authVersion`, así que la co
124
135
  repetirlo: con `authVersion: 'v2'` en `buildCredentials`, `{ environment, credentials }` ya es una
125
136
  config v2. Si también pasas `authVersion` en la config, tiene que coincidir; si no, el cliente lanza
126
137
  un `TypeError`. Las credenciales que armas a mano no llevan `authVersion`, así que con v2 la config
127
- sigue necesitando `authVersion: 'v2'`.
138
+ sigue necesitando `authVersion: 'v2'`. Si la versión solo se conoce en runtime (por ejemplo, leída
139
+ de una variable de entorno a un `BuildCredentialsOptions`), el resultado de `buildCredentials`
140
+ también va directo a la config, sin `authVersion` en ella.
128
141
 
129
142
  El PCC de las credenciales es con el que te autenticas. Trabajar en otro PCC no requiere otro
130
143
  juego de credenciales: se pasa en cada operación, como `targetPcc` en `booking.create`, `get` y
@@ -361,6 +374,7 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
361
374
  // screen.text → 'PSGR TYPE - ON' (como lo manda Sabre, sin recortar)
362
375
  // screen.warnings → los stl:Warning, si los hay
363
376
  // screen.messageId → eb:MessageId, para soporte de Sabre
377
+ // screen.respondedAt → eb:Timestamp en UTC, p. ej. '2026-11-06T17:34:16Z'
364
378
  ```
365
379
 
366
380
  - **El comando va tal cual.** En este servicio, el separador `‡` de Sabre se escribe `¥`
@@ -388,16 +402,71 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
388
402
  caracteres (`W/PT`, `DC`, `*IM`), nunca el comando entero ni la pantalla. La respuesta completa
389
403
  está disponible en `onRawResponse`.
390
404
 
405
+ ### Historial del PNR
406
+
407
+ `session.reservation.history(locator, options?)` lee el historial del PNR (Get Itinerary History,
408
+ `TravelItineraryHistoryLLSRQ` 2.4.0, el `*H` del host), una entrada por transacción:
409
+
410
+ ```ts
411
+ const { transactions } = await legacy.withSession((session) => session.reservation.history('ABC123'));
412
+ // transactions[0] → { rph, createdAt: '2026-11-04T17:15', agent, agentDutyCode, pcc, aaaPcc,
413
+ // receivedFrom, flightSegments, generalInfo, remarks, accounting }
414
+ ```
415
+
416
+ - **Quién, cuándo y desde dónde**: `createdAt` (con año, sin zona horaria), el sine y el duty code
417
+ del agente, el `pcc` de la firma, el `aaaPcc` del AAA y `receivedFrom`.
418
+ - **Qué cambió, con su `action`**: `flightSegments` con su estado en ese momento (`AS` alta, `XS`
419
+ baja; fechas sin año, como las manda el host), y SSR, ticketing, teléfonos, nombres y emails como
420
+ texto en `generalInfo` (`A4S`, `A7`, `A9`, `AN`, `APE`…). Sabre no publica la lista de valores de
421
+ `action`. Asientos, autos, hoteles, tren, seguros y facturas no se tipan: quedan en la respuesta
422
+ cruda (`onRawResponse`).
423
+ - **Trae datos del pasajero** (nombres, teléfonos, emails). El SDK nunca los manda al log.
424
+ - **No registra las colocaciones en cola**, ni las del sistema ni un `place` manual (CERT): para eso están
425
+ `queue.list`/`access` o los remarks `QUEUE_PLACE` de Get Booking.
426
+ - **Llámalo fuera de un recorrido de cola.** Dentro de una cola, Sabre rechaza el historial de otro
427
+ PNR con `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY`, un `SabreBusinessError` (CERT). El
428
+ PNR del AAA no cambia, `queue.state` sigue en `'inside'` y el recorrido sigue con `ignore`.
429
+ - Es una lectura: se reintenta. Un `locator` que no es de 6 caracteres alfanuméricos en mayúscula
430
+ lanza un `TypeError` sin llamar a la red. Un PNR que no existe lanza un `SabreBusinessError`
431
+ (`service: 'itineraryHistory'`).
432
+
391
433
  ### Colas
392
434
 
393
435
  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.
436
+ es el de la sesión. Una cola (`QueueRef`) tiene tres formas:
437
+
438
+ | Cola | `QueueRef` | Viaja como |
439
+ | - | - | - |
440
+ | Numérica, de 0 a 511 | `400` | `Number="400"` |
441
+ | Alfabética, una o dos letras mayúsculas | `'S'` | `Number="S"` |
442
+ | Con nombre (tablas QN y QNB) | `{ name: 'HOTELS' }` | `Name="HOTELS"` |
443
+
444
+ Las colas que devuelve `count` se pasan tal cual a las demás operaciones. `'S'` es la **cola de
445
+ mensajes** del PCC: tiene mensajes y no PNR, y Sabre no la lista, así que `list({ queue: 'S' })`
446
+ lanza un `TypeError` sin llamar a Sabre. Se recorre a mano: `access({ queue: 'S' })` devuelve el
447
+ primer mensaje (`kind: 'message'`) e `ignore()` el siguiente, sin sacarlo de la cola. `process` y
448
+ `place` dentro de la `S` lanzan un `TypeError` sin llamar a Sabre. `remove()` manda `QR`, pero
449
+ todavía no hay evidencia de qué hace Sabre con el mensaje. Para distinguirla, `isMessageQueue(queue)`
450
+ devuelve `true` solo con `'S'`, y `MESSAGE_QUEUE` es `'S'`:
451
+
452
+ ```ts
453
+ import { isMessageQueue } from 'sabre-client';
454
+
455
+ const { queues } = await session.queue.count();
456
+ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
457
+ ```
458
+
459
+ > **Migración desde la 0.4.x**: `count` devolvía la cola `S` como `{ name: 'S' }`, que Sabre
460
+ > rechaza (`INVALID QUEUE NAME FORMAT`). Ahora devuelve `'S'`. Un código como
461
+ > `typeof q === 'number' ? … : q.name` deja de compilar, porque `q` puede ser un `string`. Un
462
+ > `{ name }` de una o dos letras mayúsculas ahora lanza un `TypeError` que pide pasar la letra, y
463
+ > `'12'` o `'s'` también lanzan: cada cola tiene una sola forma.
396
464
 
397
465
  | Método | Servicio de Sabre | Devuelve |
398
466
  | - | - | - |
399
467
  | `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 |
468
+ | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
469
+ | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
401
470
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
402
471
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
403
472
  | `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
@@ -406,10 +475,30 @@ es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las co
406
475
 
407
476
  ```ts
408
477
  type QueueStep =
409
- | { done: false; item: QueueItem; warnings: SabreWarning[] }
478
+ | { done: false; kind: 'pnr'; item: QueueItem; warnings: SabreWarning[] }
479
+ | { done: false; kind: 'message'; message: { text: string[] }; warnings: SabreWarning[] } // cola 'S'
410
480
  | { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
411
481
  ```
412
482
 
483
+ > **Migración desde la 0.4.x**: un paso que no terminó ahora trae `kind`. Comprueba
484
+ > `step.kind === 'pnr'` antes de leer `step.item`. Solo la cola `S` devuelve `kind: 'message'`.
485
+
486
+ - **`list` devuelve la cola entera en una respuesta**: no pagina, ni con más de 1000 PNR (1221 en
487
+ unos 279 KB, en producción). `selection` la acota, pero en la lista Sabre solo acepta `QD`
488
+ (fecha de colocación en la cola, p. ej. `{ function: 'QD', criteria: '05NOV' }`) o `AK` (el
489
+ agente que lo colocó, PCC más firma, p. ej. `'PCC1AAT'`; todavía no se probó en la lista). Otro
490
+ código lanza `TypeError` sin llamar a Sabre. El `placedAt` de una entrada de la lista es cuándo
491
+ se colocó el PNR en la cola, la fecha por la que filtra `QD`.
492
+ - **`activity`** devuelve, por cola, los ocho contadores de la pantalla `QA` del host:
493
+ `numMessages` (los que están hoy en la cola), `ended`, `ignored`, `moved`, `removed`,
494
+ `transferred`, `leftMessageToContact` y `unableToReach`, más `totals` para el PCC. Sabre lista
495
+ también colas vacías con contadores. Ninguna fuente dice desde cuándo cuentan, así que el SDK los
496
+ devuelve tal cual. Como `count`, es una lectura: se reintenta, funciona dentro o fuera de una
497
+ cola y no pide duty code 9.
498
+ - **Cada resultado de colas trae `respondedAt`**: el `eb:Timestamp` de Sabre, en ISO 8601 UTC
499
+ (`'2026-11-06T17:34:16Z'`). A diferencia de `countedAt` y `placedAt`, que llegan como los manda
500
+ el host (sin año y en hora local del PCC), trae año y zona. Solo falta si Sabre no lo manda.
501
+
413
502
  - **El fin de cola es un resultado, no un error.** `END OF DISPLAY FOR REQUESTED DATA` da
414
503
  `{ done: true, reason: 'end-of-display' }`, y una cola vacía (`QUEUE SELECTED WAS EMPTY`) da
415
504
  `reason: 'empty'`. En los dos casos la sesión queda fuera de la cola. Cualquier otro error de
@@ -430,7 +519,9 @@ type QueueStep =
430
519
  0–191 o 200–254. Producción rechaza un `place` sin PIC, aunque el design lo dé como opcional.
431
520
  - **`QueueItem`** trae `locator`, el `placedAt` y el `placedBy` de `Line`, `primaryPassenger` y
432
521
  `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
522
+ sin tocar. `placedAt` es el texto que llegó, con o sin año. Dentro de una cola es la última
523
+ modificación del PNR, no su colocación en la cola: esa es el `placedAt` de `list`, y para un
524
+ mismo PNR no coinciden. `resolveHostDate` lo convierte en una
434
525
  fecha completa.
435
526
  - `count` dentro de una cola no cuenta el PNR que está en pantalla.
436
527
 
@@ -472,6 +563,19 @@ segmentos con su estado, el campo de ticketing con una marca `issued` y la líne
472
563
  - No lee los plazos que las aerolíneas escriben como texto libre en remarks, SSR u OSI. Su formato
473
564
  lo pone cada aerolínea. El texto sigue en `paragraph`.
474
565
 
566
+ ### Leer un remark `QUEUE_PLACE`
567
+
568
+ Get Booking devuelve dónde se colocó el PNR como un remark con `type: 'QUEUE_PLACE'`. Su `text`
569
+ junta el PCC, la fecha, la cola y el PIC sin separadores. `parseQueuePlaceRemark(text)` lo separa:
570
+
571
+ ```ts
572
+ parseQueuePlaceRemark('MEX25SEP146/015'); // { pcc: 'MEX', date: '25SEP', queue: 146, pic: 15 }
573
+ ```
574
+
575
+ Devuelve `null` si el texto no tiene esa forma, o si el día, el mes, la cola (0–511) o el PIC
576
+ (0–191, 200–254) están fuera de rango. `date` viene sin año: se resuelve con
577
+ `resolveHostDate(date, referenceDate)`. Leer los remarks de la reserva le toca al consumidor.
578
+
475
579
  ### Probar tu lógica de colas: `sabre-client/testing`
476
580
 
477
581
  `sabre-client/testing` es una entrada aparte, con un fake de las sesiones y las colas de Sabre. No
@@ -503,16 +607,83 @@ fake.openSessions; // 0: la sesión se cerró
503
607
  `{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A un párrafo sin línea de
504
608
  firma se le agrega una sintética. `pnrs` agrega PNR que no están en ninguna cola, para
505
609
  `place({ locators })`. El fake no tiene AAA: fuera de una cola, `place` necesita `locators`.
610
+ - `queues['S']` es la cola de mensajes: cada elemento es el texto de un mensaje (`string[]`), no
611
+ un PNR. `access` lo muestra sin `Line`, como Sabre, y `fake.messages()` da los que quedan. Que
612
+ `remove` saque el mensaje es un supuesto del fake, no se probó con Sabre.
613
+ - `queue.activity`: `numMessages` sale de las colas del fake (sin el PNR en pantalla, como en
614
+ `count`, igual que CERT). Los otros siete contadores valen 0, salvo que los siembres con
615
+ `activity: { '9': { ignored: 27 } }`. Las acciones del fake **no** los cambian: nadie probó qué
616
+ contador sube Sabre con cada acción.
617
+ - `reservation.history`: `histories: { AAAAAA: [transacciones] }` fija lo que devuelve cada PNR,
618
+ con la forma de `ReservationHistoryTransaction` (las listas que falten llegan vacías). Un PNR que
619
+ el fake conoce sin historial sembrado devuelve una transacción sintética (su creación), y un
620
+ localizador desconocido da un error de host marcado como no simulado. Dentro de una cola, el
621
+ historial de otro PNR se rechaza con `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY` y el del
622
+ PNR en pantalla se devuelve, como en CERT.
506
623
  - Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
507
624
  obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
508
625
  SDK.
509
626
  - **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.
627
+ un error en vez de la cola entera, en `access` y en `list`.
511
628
  - `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
512
- `emptyQueue`, `place`…), como `{ operation, status, body }`.
629
+ `emptyQueue`, `place`…), como `{ operation, status, body }`. Cada una tiene sus propios alias de
630
+ localizador (`walk`: `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`,
631
+ `placeWithoutInstructionCodeInside`: `AAAAAG`–`AAAAAH`), y sus líneas de firma conservan la
632
+ forma (`… <alias> H`).
633
+ - `queueFakePnrs(fixture)` convierte una grabación en los PNR del fake: primero los de `list`, en
634
+ orden, con su `Line`, y el párrafo que mostró `access`, `ignore`, `remove` o `place` para cada
635
+ uno. La cola la eliges tú, y puedes juntar fixtures. Los PNR de `queueFixtures` no traen
636
+ `primaryPassenger`: el `list` se grabó sin `primaryPassenger: true` (`/LN`), así que Sabre no
637
+ mandó el nombre. Si necesitas uno, agrégalo con un `map`:
638
+
639
+ ```ts
640
+ const fake = createQueueFake({
641
+ queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
642
+ pnrs: queueFakePnrs(queueFixtures.place),
643
+ });
644
+
645
+ const withPassenger = queueFakePnrs(queueFixtures.walk).map((pnr, index) => ({
646
+ ...pnr,
647
+ primaryPassenger: `TEST/PASSENGER${index + 1}`,
648
+ }));
649
+ ```
513
650
 
514
651
  `examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
515
652
 
653
+ #### Anonimizar respuestas grabadas
654
+
655
+ `createAnonymizer` aplica la regla con la que se grabaron las fixtures del SDK, para guardar las
656
+ respuestas de Sabre (por ejemplo, las de `onRawResponse`) sin datos de pasajeros:
657
+
658
+ ```ts
659
+ import { createAnonymizer } from 'sabre-client/testing';
660
+
661
+ const anonymizer = createAnonymizer({ secrets: [pcc, username] });
662
+ const safe = anonymizer.anonymize(raw.body); // un sobre SOAP entero
663
+ anonymizer.maskParagraph(item.paragraph); // líneas en formato de host
664
+ if (anonymizer.leaks(safe).length > 0) throw new Error('quedan datos reales');
665
+ ```
666
+
667
+ En un bundle de navegador, impórtalo de `sabre-client/anonymize`. Trae solo `createAnonymizer`,
668
+ `isSafeHostLine` y sus tipos, sin el fake ni las `queueFixtures`, que `sabre-client/testing` lleva
669
+ en un solo módulo (unos 230 KB) que los bundlers no pueden descartar:
670
+
671
+ ```ts
672
+ import { isSafeHostLine } from 'sabre-client/anonymize';
673
+ ```
674
+
675
+ - Cada localizador recibe un alias (`AAAAAA`, `AAAAAB`…), el mismo en todos los lugares donde
676
+ aparece, también en los textos que pasen después por el mismo anonimizador. `PassengerName` pasa
677
+ a `PASAJERO/UNO`, `Group` a `GRUPO`, `AgentSine` a `AAA` (salvo `SABRE`), `PseudoCityCode` a
678
+ `XXXX`, `LNIATA` a `XXXXXX` y cada `secret` a `XXXX`.
679
+ - En las líneas en formato de host (`<Text>`, o `maskLine` y `maskParagraph`), solo quedan tal cual
680
+ las de una lista blanca (`isSafeHostLine`: líneas vacías, avisos PIC como `011 SEE REMARKS`,
681
+ 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
682
+ línea de firma conserva su forma, y su localizador solo si ya es un alias.
683
+ - `leaks(text)` lista los datos reales que siguen en `text` (`'secreto'`, `'localizador'`,
684
+ `'nombre'`, `'línea del PNR'`, `'token'`): úsalo antes de escribir un archivo.
685
+ - Usa un anonimizador por grabación. Es una regla, no una garantía: revisa lo que guardas.
686
+
516
687
  ## Manejo de errores
517
688
 
518
689
  Todos los errores que lanza el SDK extienden `SabreError`:
@@ -583,6 +754,39 @@ En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `o
583
754
  > `body` es la respuesta cruda y **trae datos de pasajeros**. El SDK nunca lo manda al logger.
584
755
  > Dónde lo guardas es tu responsabilidad.
585
756
 
757
+ ## Request crudo
758
+
759
+ Las mismas operaciones aceptan `onRawRequest`, al lado de `onRawResponse`. Recibe lo que el SDK
760
+ mandó por la red, por ejemplo para mostrar el request real junto a la respuesta:
761
+
762
+ ```ts
763
+ import type { SabreRawRequest } from 'sabre-client';
764
+
765
+ let sent: SabreRawRequest | undefined;
766
+ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) });
767
+ // sent: { operation: 'booking.get', protocol: 'rest', method, url, headers, body, attempts }
768
+ ```
769
+
770
+ - `operation` es el mismo valor que recibe `onRawResponse`. `url` es la URL completa, con su query
771
+ string. `headers` son los que se mandaron, **sin `Authorization`** (se quita el header entero).
772
+ `body` es el texto que se mandó, o `undefined` si no hubo. En REST va tal cual; en
773
+ `SabreLegacy` es el sobre SOAP completo con el `BinarySecurityToken` reemplazado por
774
+ `[REDACTED]`, igual que en la respuesta.
775
+ - Se llama **una vez**, con el último intento que se mandó, antes de que la operación devuelva o
776
+ lance y antes que `onRawResponse`. A diferencia de este, se llama también si ningún intento tuvo
777
+ respuesta HTTP (error de red, abort), siempre que se haya mandado al menos uno. No se llama si la
778
+ operación lanza antes de mandar nada (por ejemplo, un `TypeError` de validación).
779
+ - En SOAP, `eb:MessageId` y `eb:Timestamp` son nuevos en cada intento, así que el sobre que llega
780
+ es el del último intento.
781
+ - No se llama con la autenticación: el request de token de OAuth, `openSession`
782
+ (`SessionCreateRQ`, cuyo sobre lleva la contraseña del EPR) y `close` (`SessionCloseRQ`).
783
+ - Es síncrono y su retorno se ignora. Si lanza, el SDK registra
784
+ `sabre.request.rawRequestHookFailed` sin el body y la operación sigue.
785
+
786
+ > [!WARNING]
787
+ > `body` y `url` **traen datos de pasajeros** (localizadores, nombres, contactos). El SDK nunca los
788
+ > manda al logger. Dónde los guardas es tu responsabilidad.
789
+
586
790
  ## Reintentos
587
791
 
588
792
  Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
@@ -638,7 +842,8 @@ const sabre = new Sabre({
638
842
  ```
639
843
 
640
844
  El SDK registra eventos estructurados (`sabre.request.start`, `sabre.request.end`,
641
- `sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
845
+ `sabre.request.retry`, `sabre.request.rawResponseHookFailed`,
846
+ `sabre.request.rawRequestHookFailed`, `sabre.auth.token.refresh`,
642
847
  `sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
643
848
  `sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
644
849
  `sabre.session.command`, `sabre.queue.exitTolerated`, `sabre.queue.exitFailed`) y nunca los bodies