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 +226 -21
- package/README.md +226 -21
- package/dist/anonymize.d.ts +54 -0
- package/dist/anonymize.js +111 -0
- package/dist/anonymize.js.map +1 -0
- package/dist/index.d.ts +2726 -693
- package/dist/index.js +497 -115
- package/dist/index.js.map +1 -1
- package/dist/reservation-history-BKRFlxpA.d.ts +792 -0
- package/dist/testing.d.ts +84 -7
- package/dist/testing.js +475 -57
- package/dist/testing.js.map +1 -1
- package/package.json +5 -1
- package/dist/queue-ref-BDGh8FrV.d.ts +0 -11
package/README.md
CHANGED
|
@@ -12,17 +12,32 @@ TypeScript SDK for the Sabre APIs.
|
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
15
|
-
`0.
|
|
16
|
-
flow over REST,
|
|
17
|
-
|
|
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.
|
|
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
|
|
391
|
-
|
|
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
|
-
| `
|
|
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.
|
|
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`,
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');\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":[]}
|