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 +207 -15
- package/README.md +207 -15
- package/dist/index.d.ts +2762 -690
- package/dist/index.js +508 -119
- package/dist/index.js.map +1 -1
- package/dist/reservation-history-bKDqkf8T.d.ts +788 -0
- package/dist/testing.d.ts +129 -7
- package/dist/testing.js +475 -57
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/queue-ref-BDGh8FrV.d.ts +0 -11
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.
|
|
16
|
-
Booking por REST,
|
|
17
|
-
|
|
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.
|
|
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
|
|
384
|
-
|
|
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
|
-
| `
|
|
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.
|
|
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`,
|
|
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.
|
|
16
|
-
flow over REST,
|
|
17
|
-
|
|
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.
|
|
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
|
|
380
|
-
|
|
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
|
-
| `
|
|
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.
|
|
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`,
|
|
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
|