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.md CHANGED
@@ -12,17 +12,32 @@ TypeScript SDK for the Sabre APIs.
12
12
 
13
13
  ## Status
14
14
 
15
- `0.4.0` is the current release of the rewrite. It covers Tier 1 and Tier 2 of the Air Booking
16
- flow over REST, and Tier 3 (SOAP sessions and queues). It adds `buildCredentials` and host
17
- commands (`session.command`) to `0.3.0`, with no breaking changes.
15
+ `0.5.1` 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`. The `0.5` line has three breaking changes (see
19
+ [Upgrading from 0.4](#upgrading-from-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
  The package is still `0.x`: a minor version may include breaking changes, and the
24
27
  [CHANGELOG](./CHANGELOG.md) marks them.
25
28
 
29
+ ### Upgrading from 0.4
30
+
31
+ - **`pcc` is no longer in the config.** Passing it to `new Sabre` or `new SabreLegacy` is a type
32
+ error, and in JavaScript it is ignored. The PCC you authenticate with goes in `credentials`
33
+ (see [Credentials](#credentials)); another PCC goes per operation, as `targetPcc` in REST or
34
+ `pcc` in the queue operations.
35
+ - **`QueueRef` can be a `string`.** An alphabetic queue like `'S'` is `'S'`, and `count` and
36
+ `place` return it that way instead of `{ name: 'S' }`. Code that does
37
+ `typeof q === 'number' ? … : q.name` must handle the `string` case.
38
+ - **A `QueueStep` that is not done has a `kind`.** Check `step.kind === 'pnr'` before reading
39
+ `step.item`; `kind: 'message'` carries a message of the `S` queue.
40
+
26
41
  Tier 1 covers the core Air Booking flow over REST:
27
42
 
28
43
  | Step | Service | Method |
@@ -45,11 +60,12 @@ Tier 3 adds the `SabreLegacy` SOAP client, with a stateful session and Queue Man
45
60
  | Step | Service | Method |
46
61
  | - | - | - |
47
62
  | 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` |
63
+ | Queues | Get Queue Count, Get Queue Activity, Access Queue, Place Queue Message | `session.queue.count`, `activity`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
64
+ | Queue paragraph | — (pure function) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
65
+ | PNR history | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
50
66
  | Host commands | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
51
67
 
52
- Planned after `0.4.0`: Get Ancillaries (NDC only).
68
+ Planned after `0.5.0`: Get Ancillaries (NDC only).
53
69
 
54
70
  ## Requirements
55
71
 
@@ -88,11 +104,6 @@ const sabre = new Sabre({
88
104
  | `fetch` | global `fetch` | Custom `fetch` (proxies, tests, other runtimes) |
89
105
  | `tokenStore` | in-memory only | Shares the token between processes (see [Token reuse](#token-reuse)) |
90
106
 
91
- > **`pcc` is deprecated** since `0.4.1` and will be removed in `0.5.0`. It was a reserved field,
92
- > and no client reads it. The PCC you authenticate with goes in `credentials` (see
93
- > [Credentials](#credentials) and `buildCredentials`). To work in another PCC, pass it per
94
- > operation: `targetPcc` in REST, `pcc` in the queue operations.
95
-
96
107
  ### Credentials
97
108
 
98
109
  There is **one set of credentials** for REST and SOAP: the same `credentials` object works for
@@ -124,7 +135,9 @@ The object that `buildCredentials` returns carries its `authVersion`, so the con
124
135
  need to repeat it: with `authVersion: 'v2'` in `buildCredentials`, `{ environment, credentials }`
125
136
  is already a v2 config. If you pass `authVersion` in the config too, it must match, or the client
126
137
  throws a `TypeError`. Credentials you build by hand have no `authVersion`, so with v2 the config
127
- still needs `authVersion: 'v2'`.
138
+ still needs `authVersion: 'v2'`. If the version is only known at runtime (for example, read
139
+ from an environment variable into a `BuildCredentialsOptions`), the result of `buildCredentials`
140
+ can still go straight into the config, with no `authVersion` there.
128
141
 
129
142
  The PCC in the credentials is the one you authenticate with. Working in another PCC is not a
130
143
  second set of credentials: pass it per operation, as `targetPcc` in `booking.create`, `get` and
@@ -360,6 +373,7 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
360
373
  // screen.text → 'PSGR TYPE - ON' (as Sabre sends it, not trimmed)
361
374
  // screen.warnings → the stl:Warning entries, if any
362
375
  // screen.messageId → eb:MessageId, for Sabre support
376
+ // screen.respondedAt → eb:Timestamp in UTC, e.g. '2026-11-06T17:34:16Z'
363
377
  ```
364
378
 
365
379
  - **The command is sent as is.** In this service Sabre's `‡` separator is written `¥`
@@ -384,16 +398,71 @@ const screen = await legacy.withSession((session) => session.command('W/PT¥*',
384
398
  characters (`W/PT`, `DC`, `*IM`), never the whole command or the screen. The full response is
385
399
  available through `onRawResponse`.
386
400
 
401
+ ### PNR history
402
+
403
+ `session.reservation.history(locator, options?)` reads the PNR history (Get Itinerary History,
404
+ `TravelItineraryHistoryLLSRQ` 2.4.0, the host's `*H`), one entry per transaction:
405
+
406
+ ```ts
407
+ const { transactions } = await legacy.withSession((session) => session.reservation.history('ABC123'));
408
+ // transactions[0] → { rph, createdAt: '2026-11-04T17:15', agent, agentDutyCode, pcc, aaaPcc,
409
+ // receivedFrom, flightSegments, generalInfo, remarks, accounting }
410
+ ```
411
+
412
+ - **Who, when and from where**: `createdAt` (with a year, no time zone), the agent's sine and duty
413
+ code, the signing `pcc`, the AAA's `aaaPcc` and `receivedFrom`.
414
+ - **What changed, with its `action`**: `flightSegments` with their status at that moment
415
+ (`AS` added, `XS` cancelled; dates without a year, as the host sends them), and SSR, ticketing,
416
+ phones, names and emails as text in `generalInfo` (`A4S`, `A7`, `A9`, `AN`, `APE`…). Sabre does
417
+ not publish the list of `action` values. Seats, cars, hotels, rail, insurance and invoices are
418
+ not typed: they stay in the raw response (`onRawResponse`).
419
+ - **It carries passenger data** (names, phones, emails). The SDK never logs it.
420
+ - **It does not record queue placements**, neither the system's nor a manual `place` (CERT): for that, use
421
+ `queue.list`/`access` or the `QUEUE_PLACE` remarks of Get Booking.
422
+ - **Call it outside a queue walk.** Inside a queue, Sabre rejects the history of another PNR with
423
+ `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY`, a `SabreBusinessError` (CERT). The PNR in
424
+ the AAA does not change, `queue.state` stays `'inside'`, and the walk goes on with `ignore`.
425
+ - It is a read: it is retried. A `locator` that is not 6 uppercase alphanumeric characters throws a
426
+ `TypeError` without calling the network. A PNR that does not exist throws a `SabreBusinessError`
427
+ (`service: 'itineraryHistory'`).
428
+
387
429
  ### Queues
388
430
 
389
431
  The queue operations live in `session.queue`. `pcc` is optional in every call and defaults to the
390
- session's PCC. A queue is a number from 0 to 511 or `{ name }` for alphabetic queues
391
- (`QueueRef`). The queues returned by `count` can be passed straight to the other operations.
432
+ session's PCC. A queue (`QueueRef`) comes in three forms:
433
+
434
+ | Queue | `QueueRef` | Sent as |
435
+ | - | - | - |
436
+ | Numeric, 0 to 511 | `400` | `Number="400"` |
437
+ | Alphabetic, one or two uppercase letters | `'S'` | `Number="S"` |
438
+ | Named (QN and QNB tables) | `{ name: 'HOTELS' }` | `Name="HOTELS"` |
439
+
440
+ The queues returned by `count` can be passed straight to the other operations. `'S'` is the PCC's
441
+ **message queue**: it holds messages, not PNRs, and Sabre does not list it, so `list({ queue: 'S' })`
442
+ throws a `TypeError` without calling Sabre. Walk it by hand: `access({ queue: 'S' })` returns the
443
+ first message (`kind: 'message'`) and `ignore()` the next one, leaving it in the queue. `process`
444
+ and `place` inside `S` throw a `TypeError` without calling Sabre. `remove()` sends `QR`, but there
445
+ is no evidence yet of what Sabre does with the message. To tell it apart, `isMessageQueue(queue)`
446
+ returns `true` only for `'S'`, and `MESSAGE_QUEUE` is `'S'`:
447
+
448
+ ```ts
449
+ import { isMessageQueue } from 'sabre-client';
450
+
451
+ const { queues } = await session.queue.count();
452
+ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
453
+ ```
454
+
455
+ > **Migrating from 0.4.x**: `count` used to return the `S` queue as `{ name: 'S' }`, which Sabre
456
+ > rejects (`INVALID QUEUE NAME FORMAT`). It now returns `'S'`. Code that does
457
+ > `typeof q === 'number' ? … : q.name` no longer compiles, because `q` can be a `string`. A
458
+ > `{ name }` of one or two uppercase letters now throws a `TypeError` that tells you to pass the
459
+ > letter instead, and `'12'` or `'s'` throw too: each queue has a single form.
392
460
 
393
461
  | Method | Sabre service | Returns |
394
462
  | - | - | - |
395
463
  | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
396
- | `list({ queue, pcc?, primaryPassenger? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
464
+ | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
465
+ | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
397
466
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
398
467
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
399
468
  | `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
@@ -402,10 +471,30 @@ session's PCC. A queue is a number from 0 to 511 or `{ name }` for alphabetic qu
402
471
 
403
472
  ```ts
404
473
  type QueueStep =
405
- | { done: false; item: QueueItem; warnings: SabreWarning[] }
474
+ | { done: false; kind: 'pnr'; item: QueueItem; warnings: SabreWarning[] }
475
+ | { done: false; kind: 'message'; message: { text: string[] }; warnings: SabreWarning[] } // queue 'S'
406
476
  | { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
407
477
  ```
408
478
 
479
+ > **Migrating from 0.4.x**: a step that is not done now has a `kind`. Check `step.kind === 'pnr'`
480
+ > before reading `step.item`. Only the `S` queue returns `kind: 'message'`.
481
+
482
+ - **`list` returns the whole queue in one response**: there is no paging, even with more than 1000
483
+ PNRs (1221 in about 279 KB, in production). `selection` narrows it, but in the list Sabre only
484
+ accepts `QD` (date placed on the queue, e.g. `{ function: 'QD', criteria: '05NOV' }`) or `AK`
485
+ (the agent who placed it, PCC plus sine, e.g. `'PCC1AAT'`; not tested in the list yet).
486
+ Any other code throws a `TypeError` without calling Sabre. The `placedAt` of a list entry is
487
+ when the PNR was placed on the queue, the date `QD` filters on.
488
+ - **`activity`** returns, per queue, the eight counters of the host's `QA` screen: `numMessages`
489
+ (on the queue now), `ended`, `ignored`, `moved`, `removed`, `transferred`,
490
+ `leftMessageToContact` and `unableToReach`, plus `totals` for the PCC. Sabre also lists empty
491
+ queues that have counters. No source says since when the counters run, so the SDK returns them
492
+ as they come. Like `count`, it is a read: it is retried, it works inside or outside a queue, and
493
+ it does not need duty code 9.
494
+ - **Every queue result carries `respondedAt`**: Sabre's `eb:Timestamp`, in ISO 8601 UTC
495
+ (`'2026-11-06T17:34:16Z'`). Unlike `countedAt` and `placedAt`, which come as the host sends them
496
+ (no year, PCC local time), it has a year and a zone. It is missing only if Sabre does not send it.
497
+
409
498
  - **The end of the queue is a result, not an error.** `END OF DISPLAY FOR REQUESTED DATA` gives
410
499
  `{ done: true, reason: 'end-of-display' }`, and an empty queue (`QUEUE SELECTED WAS EMPTY`)
411
500
  gives `reason: 'empty'`. In both cases the session is left outside the queue. Any other Sabre
@@ -425,7 +514,9 @@ type QueueStep =
425
514
  Production rejects a `place` without it, although the design says it is optional.
426
515
  - **`QueueItem`** has `locator`, the `placedAt` and `placedBy` of `Line`, `primaryPassenger` and
427
516
  `group` when Sabre sends them, and `paragraph`: the PNR in host format, line by line and
428
- untouched. `placedAt` is the text that arrived, with or without the year. `resolveHostDate`
517
+ untouched. `placedAt` is the text that arrived, with or without the year. Inside a queue it is
518
+ the PNR's last change, not when it was placed on the queue: that one is the `placedAt` of
519
+ `list`, and the two differ for the same PNR. `resolveHostDate`
429
520
  turns it into a full date.
430
521
  - `count` inside a queue does not count the PNR on screen.
431
522
 
@@ -466,6 +557,20 @@ segments with their status, the ticketing field with an `issued` flag, and the r
466
557
  - It does not read deadlines that airlines write as free text in remarks, SSR or OSI. Their format
467
558
  belongs to each airline. The text is still in `paragraph`.
468
559
 
560
+ ### Reading a `QUEUE_PLACE` remark
561
+
562
+ Get Booking returns where the PNR was placed as a remark with `type: 'QUEUE_PLACE'`. Its `text`
563
+ packs the PCC, the date, the queue and the PIC with no separators. `parseQueuePlaceRemark(text)`
564
+ splits it:
565
+
566
+ ```ts
567
+ parseQueuePlaceRemark('MEX25SEP146/015'); // { pcc: 'MEX', date: '25SEP', queue: 146, pic: 15 }
568
+ ```
569
+
570
+ It returns `null` if the text does not have that shape, or if the day, month, queue (0–511) or PIC
571
+ (0–191, 200–254) are out of range. `date` has no year: resolve it with
572
+ `resolveHostDate(date, referenceDate)`. Reading the remarks from the booking is up to you.
573
+
469
574
  ### Testing your queue logic: `sabre-client/testing`
470
575
 
471
576
  `sabre-client/testing` is a separate entry point with a fake of Sabre's sessions and queues. It is
@@ -496,16 +601,82 @@ fake.openSessions; // 0: the session was closed
496
601
  `{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A paragraph without a
497
602
  signature line gets a synthetic one. `pnrs` adds PNRs that are not in any queue, for
498
603
  `place({ locators })`. The fake has no AAA: outside a queue, `place` needs `locators`.
604
+ - `queues['S']` is the message queue: each item is a message's text (`string[]`), not a PNR.
605
+ `access` shows it without `Line`, as Sabre does, and `fake.messages()` lists the ones left.
606
+ That `remove` takes a message out is an assumption of the fake, not tested against Sabre.
607
+ - `queue.activity`: `numMessages` comes from the fake's queues (without the PNR on screen, as in
608
+ `count`, as CERT does). The other seven counters are 0 unless you seed them with
609
+ `activity: { '9': { ignored: 27 } }`. The fake's actions do **not** change them: no one has
610
+ tested which counter Sabre raises for each action.
611
+ - `reservation.history`: `histories: { AAAAAA: [transactions] }` sets what each PNR returns, in
612
+ the shape of `ReservationHistoryTransaction` (missing lists come back empty). A PNR the fake
613
+ knows without a seeded history returns one synthetic transaction (its creation), and an unknown
614
+ locator gives a host error marked as not simulated. Inside a queue, the history of another PNR
615
+ is rejected with `NAK6 -NOT ALLOWED- FINISH OR IGNORE OTHER ACTIVITY` and the one on screen is
616
+ returned, as in CERT.
499
617
  - It answers like production: empty queues, the end of the queue, `place` moving the PNR, the
500
618
  required PIC, and an expired session with `fake.expireSessions()`. `fake.calls` lists what the
501
619
  SDK asked for.
502
620
  - **Not simulated**: PCCs (the queues are the same for any `pcc`) and `selection`, which gets an
503
- error instead of the whole queue.
621
+ error instead of the whole queue, in `access` and in `list`.
504
622
  - `queueFixtures` holds the anonymized responses recorded in production (`walk`, `emptyQueue`,
505
- `place`, …), as `{ operation, status, body }`.
623
+ `place`, …), as `{ operation, status, body }`. Each one has its own locator aliases (`walk`:
624
+ `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`, `placeWithoutInstructionCodeInside`:
625
+ `AAAAAG`–`AAAAAH`), and their signature lines keep their shape (`… <alias> H`).
626
+ - `queueFakePnrs(fixture)` turns a recording into the fake's PNRs: those of `list` first, in
627
+ order, with their `Line`, and the paragraph `access`, `ignore`, `remove` or `place` showed for
628
+ each one. You choose the queue, and you can mix fixtures. The PNRs of `queueFixtures` have no
629
+ `primaryPassenger`: `list` was recorded without `primaryPassenger: true` (`/LN`), so Sabre did
630
+ not send the name. If you need one, add it with a `map`:
631
+
632
+ ```ts
633
+ const fake = createQueueFake({
634
+ queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
635
+ pnrs: queueFakePnrs(queueFixtures.place),
636
+ });
637
+
638
+ const withPassenger = queueFakePnrs(queueFixtures.walk).map((pnr, index) => ({
639
+ ...pnr,
640
+ primaryPassenger: `TEST/PASSENGER${index + 1}`,
641
+ }));
642
+ ```
506
643
 
507
644
  `examples/queue-review.mjs` is a full example, and its test runs it against the fake.
508
645
 
646
+ #### Anonymizing recorded responses
647
+
648
+ `createAnonymizer` applies the rule the SDK's fixtures were recorded with, so you can store
649
+ Sabre's responses (for example from `onRawResponse`) without passenger data:
650
+
651
+ ```ts
652
+ import { createAnonymizer } from 'sabre-client/testing';
653
+
654
+ const anonymizer = createAnonymizer({ secrets: [pcc, username] });
655
+ const safe = anonymizer.anonymize(raw.body); // a whole SOAP envelope
656
+ anonymizer.maskParagraph(item.paragraph); // host-format lines
657
+ if (anonymizer.leaks(safe).length > 0) throw new Error('real data left');
658
+ ```
659
+
660
+ In a browser bundle, import it from `sabre-client/anonymize` instead. It has only
661
+ `createAnonymizer`, `isSafeHostLine` and their types, without the fake or `queueFixtures`, which
662
+ `sabre-client/testing` carries in a single module (about 230 KB) that bundlers cannot drop:
663
+
664
+ ```ts
665
+ import { isSafeHostLine } from 'sabre-client/anonymize';
666
+ ```
667
+
668
+ - Each locator gets an alias (`AAAAAA`, `AAAAAB`…), the same one everywhere it appears, also in
669
+ later texts passed to the same anonymizer. `PassengerName` becomes `PASAJERO/UNO`, `Group`
670
+ becomes `GRUPO`, `AgentSine` becomes `AAA` (except `SABRE`), `PseudoCityCode` becomes `XXXX`,
671
+ `LNIATA` becomes `XXXXXX`, and each `secret` becomes `XXXX`.
672
+ - In host-format lines (`<Text>`, or `maskLine` and `maskParagraph`), only the lines of a
673
+ whitelist stay as they are (`isSafeHostLine`: empty lines, PIC notices like `011 SEE REMARKS`,
674
+ section titles, the end of the queue). In the others, letters become `X` and digits `9`. The
675
+ signature line keeps its shape, and its locator only if it is already an alias.
676
+ - `leaks(text)` lists the real data still in `text` (`'secreto'`, `'localizador'`, `'nombre'`,
677
+ `'línea del PNR'`, `'token'`): use it before writing a file.
678
+ - Use one anonymizer per recording. It is a rule, not a guarantee: check what you store.
679
+
509
680
  ## Error handling
510
681
 
511
682
  Every error thrown by the SDK extends `SabreError`:
@@ -576,6 +747,39 @@ In `SabreLegacy`, `protocol` is `'soap'`, `body` is the whole SOAP envelope, and
576
747
  > `body` is the raw response and **contains passenger data**. The SDK never sends it to the
577
748
  > logger. Where you store it is up to you.
578
749
 
750
+ ## Raw request
751
+
752
+ The same operations accept `onRawRequest`, next to `onRawResponse`. It receives what the SDK sent
753
+ over the network, for example to show the real request next to the response:
754
+
755
+ ```ts
756
+ import type { SabreRawRequest } from 'sabre-client';
757
+
758
+ let sent: SabreRawRequest | undefined;
759
+ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) });
760
+ // sent: { operation: 'booking.get', protocol: 'rest', method, url, headers, body, attempts }
761
+ ```
762
+
763
+ - `operation` is the same value `onRawResponse` gets. `url` is the full URL, with its query
764
+ string. `headers` are the ones sent, **without `Authorization`** (the whole header is dropped).
765
+ `body` is the text sent, or `undefined` if there was none. In REST it is untouched; in
766
+ `SabreLegacy` it is the whole SOAP envelope with the `BinarySecurityToken` replaced by
767
+ `[REDACTED]`, as in the response.
768
+ - It is called **once**, with the last attempt sent, before the operation returns or throws and
769
+ before `onRawResponse`. Unlike `onRawResponse`, it is also called when no attempt got an HTTP
770
+ response (network error, abort), as long as at least one was sent. It is not called if the
771
+ operation throws before sending anything (for example a `TypeError` from validation).
772
+ - In SOAP, `eb:MessageId` and `eb:Timestamp` are new on every attempt, so the envelope you get is
773
+ the one of the last attempt.
774
+ - It is not called for authentication: the OAuth token request, `openSession`
775
+ (`SessionCreateRQ`, whose envelope carries the EPR password) and `close` (`SessionCloseRQ`).
776
+ - It is synchronous and its return value is ignored. If it throws, the SDK logs
777
+ `sabre.request.rawRequestHookFailed` without the body, and the operation carries on.
778
+
779
+ > [!WARNING]
780
+ > `body` and `url` **contain passenger data** (locators, names, contact details). The SDK never
781
+ > sends them to the logger. Where you store them is up to you.
782
+
579
783
  ## Retries
580
784
 
581
785
  Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
@@ -632,7 +836,8 @@ const sabre = new Sabre({
632
836
  ```
633
837
 
634
838
  The SDK logs structured events (`sabre.request.start`, `sabre.request.end`,
635
- `sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
839
+ `sabre.request.retry`, `sabre.request.rawResponseHookFailed`,
840
+ `sabre.request.rawRequestHookFailed`, `sabre.auth.token.refresh`,
636
841
  `sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
637
842
  `sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
638
843
  `sabre.session.command`, `sabre.queue.exitTolerated`, `sabre.queue.exitFailed`), never raw
@@ -0,0 +1,54 @@
1
+ /** Qué dato real quedó en un texto: lo que devuelve `Anonymizer.leaks`. */
2
+ type AnonymizerLeak = 'secreto' | 'localizador' | 'nombre' | 'línea del PNR' | 'token';
3
+ interface AnonymizerOptions {
4
+ /**
5
+ * Valores que nunca deben quedar: credenciales, usuario, PCC. Cada uno pasa a `XXXX` donde
6
+ * aparezca. Se ignoran los que no son string o tienen menos de 3 caracteres.
7
+ */
8
+ secrets?: ReadonlyArray<string | undefined>;
9
+ }
10
+ /**
11
+ * Una anonimización con memoria: el mismo localizador da siempre el mismo alias y el mismo
12
+ * pasajero el mismo nombre falso, en todos los textos que pasen por ella. Una por grabación.
13
+ */
14
+ interface Anonymizer {
15
+ /**
16
+ * Un sobre SOAP o cualquier texto con su XML (también un JSON con XML adentro):
17
+ * - cada `UniqueID@ID` pasa a un alias (`AAAAAA`, `AAAAAB`…), y el localizador real se reemplaza
18
+ * por su alias en todo el texto;
19
+ * - `PassengerName` a `PASAJERO/UNO`, `PASAJERO/DOS`…, y `Group` a `GRUPO`;
20
+ * - `AgentSine` a `AAA` (salvo `SABRE`), `PseudoCityCode` a `XXXX` y `LNIATA` a `XXXXXX`;
21
+ * - cada `<Text>` pasa por `maskLine`;
22
+ * - los `secrets`, a `XXXX`.
23
+ */
24
+ anonymize(text: string): string;
25
+ /**
26
+ * Una línea del PNR en formato de host. Las de la lista blanca (`isSafeHostLine`) quedan tal
27
+ * cual; en las demás, letras a `X` y dígitos a `9`. La cabecera `QUEUE LIST FOR <PCC>` solo
28
+ * tapa el PCC, y `RECEIVED FROM - ` conserva el prefijo. La línea de firma
29
+ * (`… HHMM/DDMMMYY <localizador> H`) conserva su forma, y el localizador solo si ya es un alias:
30
+ * de ella sale el localizador del PNR siguiente en `place` dentro de una cola. Llamarla después
31
+ * de dar alias a los localizadores.
32
+ */
33
+ maskLine(line: string): string;
34
+ /** `maskLine` sobre cada línea de un párrafo, p. ej. `QueueItem.paragraph`. */
35
+ maskParagraph(lines: readonly string[]): string[];
36
+ /** El alias de un localizador: lo asigna la primera vez, en orden (`AAAAAA`, `AAAAAB`…). */
37
+ aliasOf(locator: string): string;
38
+ /**
39
+ * Qué datos reales siguen en `text`, sin repetir: un `secret`, un localizador o un nombre que ya
40
+ * tienen alias, una línea del PNR que se enmascaró, o un token de sesión. Vacío si no queda
41
+ * ninguno. Sirve para no escribir una fixture que todavía trae datos reales.
42
+ */
43
+ leaks(text: string): AnonymizerLeak[];
44
+ }
45
+ /**
46
+ * Si una línea del PNR en formato de host queda tal cual al anonimizar: está en la lista blanca de
47
+ * #96 (líneas vacías, avisos PIC como `011 SEE REMARKS`, títulos de sección como `REMARKS`, el fin
48
+ * de cola).
49
+ */
50
+ declare function isSafeHostLine(line: string): boolean;
51
+ /** Ver `Anonymizer`. */
52
+ declare function createAnonymizer(options?: AnonymizerOptions): Anonymizer;
53
+
54
+ export { type Anonymizer, type AnonymizerLeak, type AnonymizerOptions, createAnonymizer, isSafeHostLine };
@@ -0,0 +1,111 @@
1
+ // src/testing/anonymize.ts
2
+ var NAMES = ["UNO", "DOS", "TRES", "CUATRO", "CINCO", "SEIS", "SIETE", "OCHO", "NUEVE", "DIEZ"];
3
+ var SAFE_LINES = [
4
+ /^\s*$/,
5
+ // Aviso PIC: 011 SEE REMARKS. Sin dígitos después del número: una línea de `list`
6
+ // (`100 ABC123 PLT MAM 01JUN26 1948`) también empieza con 3 dígitos y trae el sine.
7
+ /^\s*\d{3}\s{2,}[A-Z][A-Z ,./-]*$/,
8
+ /^\s*END OF DISPLAY FOR REQUESTED DATA\s*$/,
9
+ /^\s*(TKT\/TIME LIMIT|PHONES|REMARKS|ADDRESS|INVOICED|GENERAL FACTS|[A-Z0-9]{2} FACTS|NO ITIN)\s*$/,
10
+ /^\s*PASSENGER EMAIL DATA EXISTS\s+\*PE TO DISPLAY ALL\s*$/
11
+ ];
12
+ function isSafeHostLine(line) {
13
+ return SAFE_LINES.some((pattern) => pattern.test(line));
14
+ }
15
+ var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
16
+ var decode = (text) => text.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => ENTITIES[entity] ?? "");
17
+ var encode = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
18
+ var masked = (text) => text.replace(/\p{L}/gu, "X").replace(/\p{N}/gu, "9");
19
+ function createAnonymizer(options = {}) {
20
+ const secrets = (options.secrets ?? []).filter((value) => typeof value === "string" && value.length >= 3).sort((a, b) => b.length - a.length);
21
+ const redactSecrets = (text) => secrets.reduce((value, secret) => value.split(secret).join("XXXX"), text);
22
+ const locatorAliases = /* @__PURE__ */ new Map();
23
+ const realNames = /* @__PURE__ */ new Map();
24
+ const maskedLines = /* @__PURE__ */ new Set();
25
+ const aliasOf = (locator) => {
26
+ let alias = locatorAliases.get(locator);
27
+ if (alias === void 0) {
28
+ const n = locatorAliases.size;
29
+ const letters = [17576, 676, 26, 1].map((size) => String.fromCharCode(65 + Math.floor(n / size) % 26)).join("");
30
+ alias = `AA${letters}`;
31
+ locatorAliases.set(locator, alias);
32
+ }
33
+ return alias;
34
+ };
35
+ const fakeName = (name) => {
36
+ let fake = realNames.get(name);
37
+ if (fake === void 0) {
38
+ fake = `PASAJERO/${NAMES[realNames.size] ?? realNames.size + 1}`;
39
+ realNames.set(name, fake);
40
+ }
41
+ return fake;
42
+ };
43
+ function anonymizeLine(line) {
44
+ if (isSafeHostLine(line)) return line;
45
+ const header = /^(\s*QUEUE LIST FOR )\S+(\s+QUEUE )(\S+)\s*$/.exec(line);
46
+ if (header !== null) return `${header[1]}XXXX${header[2]}${header[3]}`;
47
+ const received = /^(\s*RECEIVED FROM - )(.*)$/.exec(line);
48
+ const signature = /^(.*\s)(\d{4}\/\d{2}[A-Z]{3}\d{2})(\s+)([A-Z0-9]{6})(\s+H\s*)$/.exec(line);
49
+ const aliases = new Set(locatorAliases.values());
50
+ let result;
51
+ if (received !== null) {
52
+ result = `${received[1]}${masked(received[2] ?? "")}`;
53
+ } else if (signature !== null) {
54
+ const [, before = "", stamp = "", gap = "", locator = "", end = ""] = signature;
55
+ result = `${masked(before)}${masked(stamp)}${gap}${aliases.has(locator) ? locator : masked(locator)}${end}`;
56
+ } else {
57
+ result = masked(line);
58
+ }
59
+ if (line.trim().length >= 4 && result.trim() !== line.trim()) maskedLines.add(line.trim());
60
+ return result;
61
+ }
62
+ function anonymize(body) {
63
+ let text = String(body).replace(
64
+ /(<(?:\w+:)?UniqueID\b[^>]*\bID=")([^"]+)(")/g,
65
+ (_, a, locator, b) => `${a}${aliasOf(locator)}${b}`
66
+ );
67
+ for (const [locator, alias] of locatorAliases) text = text.split(locator).join(alias);
68
+ text = text.replace(
69
+ /(<(?:\w+:)?PassengerName>)([^<]*)(<\/)/g,
70
+ (_, a, name, b) => `${a}${fakeName(decode(name))}${b}`
71
+ );
72
+ text = text.replace(
73
+ /(<(?:\w+:)?Group>)([^<]*)(<\/)/g,
74
+ (_, a, value, b) => value === "" ? `${a}${b}` : `${a}GRUPO${b}`
75
+ );
76
+ text = text.replace(
77
+ /(\bAgentSine=")([^"]*)(")/g,
78
+ (_, a, sine, b) => `${a}${sine === "SABRE" ? sine : "AAA"}${b}`
79
+ );
80
+ text = text.replace(/(\bPseudoCityCode=")([^"]*)(")/g, "$1XXXX$3");
81
+ text = text.replace(/(\bLNIATA=")([^"]*)(")/g, "$1XXXXXX$3");
82
+ text = text.replace(
83
+ /(<(?:\w+:)?Text\b[^>/]*>)([\s\S]*?)(<\/(?:\w+:)?Text>)/g,
84
+ (_, a, line, b) => `${a}${encode(anonymizeLine(decode(line)))}${b}`
85
+ );
86
+ return redactSecrets(text);
87
+ }
88
+ const maskLine = (line) => redactSecrets(anonymizeLine(line));
89
+ function leaks(text) {
90
+ const found = [
91
+ ...secrets.filter((secret) => text.includes(secret)).map(() => "secreto"),
92
+ ...[...locatorAliases.keys()].filter((locator) => text.includes(locator)).map(() => "localizador"),
93
+ ...[...realNames.keys()].filter((realName) => realName.length >= 3 && text.includes(realName)).map(() => "nombre"),
94
+ ...[...maskedLines].filter((line) => text.includes(line)).map(() => "l\xEDnea del PNR"),
95
+ .../IceSess|Shared\/IDL/.test(text) ? ["token"] : []
96
+ ];
97
+ return [...new Set(found)];
98
+ }
99
+ return {
100
+ anonymize,
101
+ maskLine,
102
+ maskParagraph: (lines) => lines.map(maskLine),
103
+ aliasOf,
104
+ leaks
105
+ };
106
+ }
107
+ export {
108
+ createAnonymizer,
109
+ isSafeHostLine
110
+ };
111
+ //# sourceMappingURL=anonymize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/anonymize.ts"],"sourcesContent":["// Anonimización de las respuestas de colas grabadas (la regla de #96, ADR-018), la misma que usan\n// los scripts que graban fixtures. Exportada para que los consumidores no la repitan (ADR-027, D1).\n// Solo Web Standard APIs: corre en cualquier runtime.\n\n/** Qué dato real quedó en un texto: lo que devuelve `Anonymizer.leaks`. */\nexport type AnonymizerLeak = 'secreto' | 'localizador' | 'nombre' | 'línea del PNR' | 'token';\n\nexport interface AnonymizerOptions {\n /**\n * Valores que nunca deben quedar: credenciales, usuario, PCC. Cada uno pasa a `XXXX` donde\n * aparezca. Se ignoran los que no son string o tienen menos de 3 caracteres.\n */\n secrets?: ReadonlyArray<string | undefined>;\n}\n\n/**\n * Una anonimización con memoria: el mismo localizador da siempre el mismo alias y el mismo\n * pasajero el mismo nombre falso, en todos los textos que pasen por ella. Una por grabación.\n */\nexport interface Anonymizer {\n /**\n * Un sobre SOAP o cualquier texto con su XML (también un JSON con XML adentro):\n * - cada `UniqueID@ID` pasa a un alias (`AAAAAA`, `AAAAAB`…), y el localizador real se reemplaza\n * por su alias en todo el texto;\n * - `PassengerName` a `PASAJERO/UNO`, `PASAJERO/DOS`…, y `Group` a `GRUPO`;\n * - `AgentSine` a `AAA` (salvo `SABRE`), `PseudoCityCode` a `XXXX` y `LNIATA` a `XXXXXX`;\n * - cada `<Text>` pasa por `maskLine`;\n * - los `secrets`, a `XXXX`.\n */\n anonymize(text: string): string;\n /**\n * Una línea del PNR en formato de host. Las de la lista blanca (`isSafeHostLine`) quedan tal\n * cual; en las demás, letras a `X` y dígitos a `9`. La cabecera `QUEUE LIST FOR <PCC>` solo\n * tapa el PCC, y `RECEIVED FROM - ` conserva el prefijo. La línea de firma\n * (`… HHMM/DDMMMYY <localizador> H`) conserva su forma, y el localizador solo si ya es un alias:\n * de ella sale el localizador del PNR siguiente en `place` dentro de una cola. Llamarla después\n * de dar alias a los localizadores.\n */\n maskLine(line: string): string;\n /** `maskLine` sobre cada línea de un párrafo, p. ej. `QueueItem.paragraph`. */\n maskParagraph(lines: readonly string[]): string[];\n /** El alias de un localizador: lo asigna la primera vez, en orden (`AAAAAA`, `AAAAAB`…). */\n aliasOf(locator: string): string;\n /**\n * Qué datos reales siguen en `text`, sin repetir: un `secret`, un localizador o un nombre que ya\n * tienen alias, una línea del PNR que se enmascaró, o un token de sesión. Vacío si no queda\n * ninguno. Sirve para no escribir una fixture que todavía trae datos reales.\n */\n leaks(text: string): AnonymizerLeak[];\n}\n\nconst NAMES = ['UNO', 'DOS', 'TRES', 'CUATRO', 'CINCO', 'SEIS', 'SIETE', 'OCHO', 'NUEVE', 'DIEZ'];\n\n/** Líneas de un PNR en formato de host que quedan tal cual: no traen datos del pasajero. */\nconst SAFE_LINES: readonly RegExp[] = [\n /^\\s*$/,\n // Aviso PIC: 011 SEE REMARKS. Sin dígitos después del número: una línea de `list`\n // (`100 ABC123 PLT MAM 01JUN26 1948`) también empieza con 3 dígitos y trae el sine.\n /^\\s*\\d{3}\\s{2,}[A-Z][A-Z ,./-]*$/,\n /^\\s*END OF DISPLAY FOR REQUESTED DATA\\s*$/,\n /^\\s*(TKT\\/TIME LIMIT|PHONES|REMARKS|ADDRESS|INVOICED|GENERAL FACTS|[A-Z0-9]{2} FACTS|NO ITIN)\\s*$/,\n /^\\s*PASSENGER EMAIL DATA EXISTS\\s+\\*PE TO DISPLAY ALL\\s*$/,\n];\n\n/**\n * Si una línea del PNR en formato de host queda tal cual al anonimizar: está en la lista blanca de\n * #96 (líneas vacías, avisos PIC como `011 SEE REMARKS`, títulos de sección como `REMARKS`, el fin\n * de cola).\n */\nexport function isSafeHostLine(line: string): boolean {\n return SAFE_LINES.some((pattern) => pattern.test(line));\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\" };\nconst decode = (text: string) => text.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity: string) => ENTITIES[entity] ?? '');\nconst encode = (text: string) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst masked = (text: string) => text.replace(/\\p{L}/gu, 'X').replace(/\\p{N}/gu, '9');\n\n/** Ver `Anonymizer`. */\nexport function createAnonymizer(options: AnonymizerOptions = {}): Anonymizer {\n const secrets = (options.secrets ?? [])\n .filter((value): value is string => typeof value === 'string' && value.length >= 3)\n .sort((a, b) => b.length - a.length);\n const redactSecrets = (text: string) => secrets.reduce((value, secret) => value.split(secret).join('XXXX'), text);\n const locatorAliases = new Map<string, string>();\n const realNames = new Map<string, string>();\n const maskedLines = new Set<string>();\n\n const aliasOf = (locator: string): string => {\n let alias = locatorAliases.get(locator);\n if (alias === undefined) {\n const n = locatorAliases.size;\n // Base 26 en cuatro letras: `AAAAAA`, `AAAAAB`… y sigue en letras después del 676 (`AAABAA`).\n const letters = [17576, 676, 26, 1].map((size) => String.fromCharCode(65 + (Math.floor(n / size) % 26))).join('');\n alias = `AA${letters}`;\n locatorAliases.set(locator, alias);\n }\n return alias;\n };\n const fakeName = (name: string): string => {\n let fake = realNames.get(name);\n if (fake === undefined) {\n fake = `PASAJERO/${NAMES[realNames.size] ?? realNames.size + 1}`;\n realNames.set(name, fake);\n }\n return fake;\n };\n\n function anonymizeLine(line: string): string {\n if (isSafeHostLine(line)) return line;\n const header = /^(\\s*QUEUE LIST FOR )\\S+(\\s+QUEUE )(\\S+)\\s*$/.exec(line);\n if (header !== null) return `${header[1]}XXXX${header[2]}${header[3]}`;\n const received = /^(\\s*RECEIVED FROM - )(.*)$/.exec(line);\n const signature = /^(.*\\s)(\\d{4}\\/\\d{2}[A-Z]{3}\\d{2})(\\s+)([A-Z0-9]{6})(\\s+H\\s*)$/.exec(line);\n const aliases = new Set(locatorAliases.values());\n let result: string;\n if (received !== null) {\n result = `${received[1]}${masked(received[2] ?? '')}`;\n } else if (signature !== null) {\n const [, before = '', stamp = '', gap = '', locator = '', end = ''] = signature;\n result = `${masked(before)}${masked(stamp)}${gap}${aliases.has(locator) ? locator : masked(locator)}${end}`;\n } else {\n result = masked(line);\n }\n if (line.trim().length >= 4 && result.trim() !== line.trim()) maskedLines.add(line.trim());\n return result;\n }\n\n function anonymize(body: string): string {\n let text = String(body).replace(\n /(<(?:\\w+:)?UniqueID\\b[^>]*\\bID=\")([^\"]+)(\")/g,\n (_, a: string, locator: string, b: string) => `${a}${aliasOf(locator)}${b}`,\n );\n for (const [locator, alias] of locatorAliases) text = text.split(locator).join(alias);\n text = text.replace(\n /(<(?:\\w+:)?PassengerName>)([^<]*)(<\\/)/g,\n (_, a: string, name: string, b: string) => `${a}${fakeName(decode(name))}${b}`,\n );\n text = text.replace(/(<(?:\\w+:)?Group>)([^<]*)(<\\/)/g, (_, a: string, value: string, b: string) =>\n value === '' ? `${a}${b}` : `${a}GRUPO${b}`,\n );\n text = text.replace(/(\\bAgentSine=\")([^\"]*)(\")/g, (_, a: string, sine: string, b: string) =>\n `${a}${sine === 'SABRE' ? sine : 'AAA'}${b}`,\n );\n text = text.replace(/(\\bPseudoCityCode=\")([^\"]*)(\")/g, '$1XXXX$3');\n // La dirección de la terminal (`stl:HostCommand@LNIATA`, en `session.command()`).\n text = text.replace(/(\\bLNIATA=\")([^\"]*)(\")/g, '$1XXXXXX$3');\n text = text.replace(\n /(<(?:\\w+:)?Text\\b[^>/]*>)([\\s\\S]*?)(<\\/(?:\\w+:)?Text>)/g,\n (_, a: string, line: string, b: string) => `${a}${encode(anonymizeLine(decode(line)))}${b}`,\n );\n return redactSecrets(text);\n }\n\n const maskLine = (line: string) => redactSecrets(anonymizeLine(line));\n\n function leaks(text: string): AnonymizerLeak[] {\n const found: AnonymizerLeak[] = [\n ...secrets.filter((secret) => text.includes(secret)).map((): AnonymizerLeak => 'secreto'),\n ...[...locatorAliases.keys()].filter((locator) => text.includes(locator)).map((): AnonymizerLeak => 'localizador'),\n ...[...realNames.keys()]\n .filter((realName) => realName.length >= 3 && text.includes(realName))\n .map((): AnonymizerLeak => 'nombre'),\n ...[...maskedLines].filter((line) => text.includes(line)).map((): AnonymizerLeak => 'línea del PNR'),\n ...(/IceSess|Shared\\/IDL/.test(text) ? (['token'] as const) : []),\n ];\n return [...new Set(found)];\n }\n\n return {\n anonymize,\n maskLine,\n maskParagraph: (lines) => lines.map(maskLine),\n aliasOf,\n leaks,\n };\n}\n"],"mappings":"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","names":[]}