sabre-client 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md CHANGED
@@ -12,14 +12,16 @@ SDK de TypeScript para las APIs de Sabre.
12
12
 
13
13
  ## Estado
14
14
 
15
- La `0.5.0` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.4.1`.
15
+ La `0.6.0` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.5.1`.
16
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
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
+ `onRawRequest`. La `0.6.0` suma filtros de cola: `count` con una cola, un rango o `selection`,
19
+ `activity` con una cola o un rango, y `selection` en el fake de colas. No trae cambios
20
+ incompatibles; la línea `0.5` trae tres (ver [Actualizar desde la 0.4](#actualizar-desde-la-04)).
19
21
 
20
22
  ```sh
21
- npm install sabre-client@next # 0.5.0
22
- npm install sabre-client # 0.4.1
23
+ npm install sabre-client@next # 0.6.0
24
+ npm install sabre-client # 0.5.1
23
25
  ```
24
26
 
25
27
  El paquete sigue en `0.x`: una versión minor puede traer cambios incompatibles, y el
@@ -445,7 +447,15 @@ mensajes** del PCC: tiene mensajes y no PNR, y Sabre no la lista, así que `list
445
447
  lanza un `TypeError` sin llamar a Sabre. Se recorre a mano: `access({ queue: 'S' })` devuelve el
446
448
  primer mensaje (`kind: 'message'`) e `ignore()` el siguiente, sin sacarlo de la cola. `process` y
447
449
  `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.
450
+ todavía no hay evidencia de qué hace Sabre con el mensaje. Para distinguirla, `isMessageQueue(queue)`
451
+ devuelve `true` solo con `'S'`, y `MESSAGE_QUEUE` es `'S'`:
452
+
453
+ ```ts
454
+ import { isMessageQueue } from 'sabre-client';
455
+
456
+ const { queues } = await session.queue.count();
457
+ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
458
+ ```
449
459
 
450
460
  > **Migración desde la 0.4.x**: `count` devolvía la cola `S` como `{ name: 'S' }`, que Sabre
451
461
  > rechaza (`INVALID QUEUE NAME FORMAT`). Ahora devuelve `'S'`. Un código como
@@ -455,8 +465,8 @@ todavía no hay evidencia de qué hace Sabre con el mensaje.
455
465
 
456
466
  | Método | Servicio de Sabre | Devuelve |
457
467
  | - | - | - |
458
- | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
459
- | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
468
+ | `count({ pcc?, queue?, range?, selection? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, additionalInfo?, warnings }` |
469
+ | `activity({ pcc?, queue?, range? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
460
470
  | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
461
471
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
462
472
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
@@ -480,12 +490,29 @@ type QueueStep =
480
490
  agente que lo colocó, PCC más firma, p. ej. `'PCC1AAT'`; todavía no se probó en la lista). Otro
481
491
  código lanza `TypeError` sin llamar a Sabre. El `placedAt` de una entrada de la lista es cuándo
482
492
  se colocó el PNR en la cola, la fecha por la que filtra `QD`.
493
+ - **Filtros de `count`**: `queue` cuenta una sola cola, y `range: { start, end }`, un rango de
494
+ colas numéricas; no se combinan. Con `range`, Sabre devuelve cada cola del rango, **también las
495
+ vacías**, con `count: 0` (sin filtros las omite). `selection` admite hasta 8 filtros, como
496
+ `access`, y cuenta solo los PNR que cumplen: el host acepta cualquier código (`QD`, `AK`, `AL`,
497
+ `PI`, `AC`… y combinaciones), y con el mismo `QD` la cuenta coincide con `list`. Necesita
498
+ `queue`: sin cola, Sabre responde `QUEUE SELECTED NOT VALID OR CLOSED`, y con un rango,
499
+ `KEYS NOT ALLOWED`, así que el SDK lanza `TypeError` antes. Con `selection`, `totals.unfiltered`
500
+ es cuántos PNR tiene la cola sin filtrar (el `1 MATCHING PNRS OUT OF 80` del host); con `queue`
501
+ sola, Sabre no manda `totals`. `additionalInfo` lee `AdditionalQueueInfo` (una cola transferida a
502
+ otra), tipado desde el design: ninguna respuesta real lo trajo todavía.
503
+
504
+ ```ts
505
+ const { queues, totals } = await session.queue.count({ queue: 20, selection: [{ function: 'QD', criteria: '08NOV' }] });
506
+ // queues: [{ queue: 20, count: 1 }], totals: { unfiltered: 80 }
507
+ ```
483
508
  - **`activity`** devuelve, por cola, los ocho contadores de la pantalla `QA` del host:
484
509
  `numMessages` (los que están hoy en la cola), `ended`, `ignored`, `moved`, `removed`,
485
510
  `transferred`, `leftMessageToContact` y `unableToReach`, más `totals` para el PCC. Sabre lista
486
511
  también colas vacías con contadores. Ninguna fuente dice desde cuándo cuentan, así que el SDK los
487
512
  devuelve tal cual. Como `count`, es una lectura: se reintenta, funciona dentro o fuera de una
488
- cola y no pide duty code 9.
513
+ cola y no pide duty code 9. Acepta los mismos `queue` o `range` que `count` (no los dos): con
514
+ `queue`, solo esa cola y sin `totals`; con `range`, cada cola del rango, también las que tienen
515
+ todos los contadores en 0, y los `totals` del rango.
489
516
  - **Cada resultado de colas trae `respondedAt`**: el `eb:Timestamp` de Sabre, en ISO 8601 UTC
490
517
  (`'2026-11-06T17:34:16Z'`). A diferencia de `countedAt` y `placedAt`, que llegan como los manda
491
518
  el host (sin año y en hora local del PCC), trae año y zona. Solo falta si Sabre no lo manda.
@@ -614,8 +641,25 @@ fake.openSessions; // 0: la sesión se cerró
614
641
  - Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
615
642
  obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
616
643
  SDK.
617
- - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni `selection`, que recibe
618
- un error en vez de la cola entera, en `access` y en `list`.
644
+ - `queue.count` y `queue.activity` con `queue` o `range` responden como producción: una cola sin
645
+ `totals`, o cada cola del rango, también las vacías.
646
+ - `selection` se simula para `QD`, `AK`, `AL`, `PI` y `AC` (`QUEUE_FAKE_SELECTION_FUNCTIONS`), en
647
+ `access`, `list` y `count`, combinados con Y:
648
+ - `QD` filtra por `placedAt` (`'05NOV'` o `'5NOV'`);
649
+ - `AK` filtra por `placedBy`, PCC más firma (`'ABCDAAT'`);
650
+ - `AL` filtra por la aerolínea de los segmentos aéreos del párrafo, y `AC` por su estado (una
651
+ letra cubre todos los estados que empiezan con ella);
652
+ - `PI` filtra por el PIC con que lo colocó `place` o, si no, por el aviso de PIC del párrafo
653
+ (`011 SEE REMARKS`).
654
+
655
+ `access` recorre solo los PNR que cumplen y termina en `END OF DISPLAY`, también al entrar si
656
+ ninguno cumple (un supuesto del fake), e `ignore`, `remove` y `place` saltan los demás. `list`
657
+ devuelve solo las `Line` que cumplen y, como Sabre, rechaza un código que no sea `QD` ni `AK`.
658
+ `count` con `queue` cuenta los que cumplen y devuelve `totals.unfiltered`; con `range` o sin
659
+ cola, da los errores de Sabre. Otro código recibe un error de host marcado como no simulado,
660
+ nunca la cola entera.
661
+ - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni las reglas de
662
+ combinación de `selection` del host.
619
663
  - `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
620
664
  `emptyQueue`, `place`…), como `{ operation, status, body }`. Cada una tiene sus propios alias de
621
665
  localizador (`walk`: `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`,
@@ -623,13 +667,20 @@ fake.openSessions; // 0: la sesión se cerró
623
667
  forma (`… <alias> H`).
624
668
  - `queueFakePnrs(fixture)` convierte una grabación en los PNR del fake: primero los de `list`, en
625
669
  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:
670
+ uno. La cola la eliges tú, y puedes juntar fixtures. Los PNR de `queueFixtures` no traen
671
+ `primaryPassenger`: el `list` se grabó sin `primaryPassenger: true` (`/LN`), así que Sabre no
672
+ mandó el nombre. Si necesitas uno, agrégalo con un `map`:
627
673
 
628
674
  ```ts
629
675
  const fake = createQueueFake({
630
676
  queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
631
677
  pnrs: queueFakePnrs(queueFixtures.place),
632
678
  });
679
+
680
+ const withPassenger = queueFakePnrs(queueFixtures.walk).map((pnr, index) => ({
681
+ ...pnr,
682
+ primaryPassenger: `TEST/PASSENGER${index + 1}`,
683
+ }));
633
684
  ```
634
685
 
635
686
  `examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
@@ -648,6 +699,14 @@ anonymizer.maskParagraph(item.paragraph); // líneas en formato de host
648
699
  if (anonymizer.leaks(safe).length > 0) throw new Error('quedan datos reales');
649
700
  ```
650
701
 
702
+ En un bundle de navegador, impórtalo de `sabre-client/anonymize`. Trae solo `createAnonymizer`,
703
+ `isSafeHostLine` y sus tipos, sin el fake ni las `queueFixtures`, que `sabre-client/testing` lleva
704
+ en un solo módulo (unos 230 KB) que los bundlers no pueden descartar:
705
+
706
+ ```ts
707
+ import { isSafeHostLine } from 'sabre-client/anonymize';
708
+ ```
709
+
651
710
  - Cada localizador recibe un alias (`AAAAAA`, `AAAAAB`…), el mismo en todos los lugares donde
652
711
  aparece, también en los textos que pasen después por el mismo anonimizador. `PassengerName` pasa
653
712
  a `PASAJERO/UNO`, `Group` a `GRUPO`, `AgentSine` a `AAA` (salvo `SABRE`), `PseudoCityCode` a
package/README.md CHANGED
@@ -12,14 +12,16 @@ TypeScript SDK for the Sabre APIs.
12
12
 
13
13
  ## Status
14
14
 
15
- `0.5.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.4.1`.
15
+ `0.6.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.5.1`.
16
16
  It covers Tier 1 and Tier 2 of the Air Booking flow over REST, Tier 3 (SOAP sessions and queues)
17
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
+ `onRawRequest`. `0.6.0` adds queue filters: `count` with one queue, a range or `selection`,
19
+ `activity` with one queue or a range, and `selection` in the queue fake. It has no breaking
20
+ changes; the `0.5` line has three (see [Upgrading from 0.4](#upgrading-from-04)).
19
21
 
20
22
  ```sh
21
- npm install sabre-client@next # 0.5.0
22
- npm install sabre-client # 0.4.1
23
+ npm install sabre-client@next # 0.6.0
24
+ npm install sabre-client # 0.5.1
23
25
  ```
24
26
 
25
27
  The package is still `0.x`: a minor version may include breaking changes, and the
@@ -441,7 +443,15 @@ The queues returned by `count` can be passed straight to the other operations. `
441
443
  throws a `TypeError` without calling Sabre. Walk it by hand: `access({ queue: 'S' })` returns the
442
444
  first message (`kind: 'message'`) and `ignore()` the next one, leaving it in the queue. `process`
443
445
  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.
446
+ is no evidence yet of what Sabre does with the message. To tell it apart, `isMessageQueue(queue)`
447
+ returns `true` only for `'S'`, and `MESSAGE_QUEUE` is `'S'`:
448
+
449
+ ```ts
450
+ import { isMessageQueue } from 'sabre-client';
451
+
452
+ const { queues } = await session.queue.count();
453
+ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
454
+ ```
445
455
 
446
456
  > **Migrating from 0.4.x**: `count` used to return the `S` queue as `{ name: 'S' }`, which Sabre
447
457
  > rejects (`INVALID QUEUE NAME FORMAT`). It now returns `'S'`. Code that does
@@ -451,8 +461,8 @@ is no evidence yet of what Sabre does with the message.
451
461
 
452
462
  | Method | Sabre service | Returns |
453
463
  | - | - | - |
454
- | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
455
- | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
464
+ | `count({ pcc?, queue?, range?, selection? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, additionalInfo?, warnings }` |
465
+ | `activity({ pcc?, queue?, range? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
456
466
  | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
457
467
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
458
468
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
@@ -476,12 +486,30 @@ type QueueStep =
476
486
  (the agent who placed it, PCC plus sine, e.g. `'PCC1AAT'`; not tested in the list yet).
477
487
  Any other code throws a `TypeError` without calling Sabre. The `placedAt` of a list entry is
478
488
  when the PNR was placed on the queue, the date `QD` filters on.
489
+ - **`count` filters**: `queue` counts one queue, and `range: { start, end }` a range of numeric
490
+ queues; the two do not combine. With `range`, Sabre returns every queue in the range, **empty
491
+ ones included** with `count: 0` (without filters it leaves them out). `selection` takes up to 8
492
+ filters, like `access`, and counts only the PNRs that match: the host accepts any code (`QD`,
493
+ `AK`, `AL`, `PI`, `AC`… and combinations), and with the same `QD` the count matches `list`. It
494
+ needs `queue`: without a queue Sabre answers `QUEUE SELECTED NOT VALID OR CLOSED`, and with a
495
+ range, `KEYS NOT ALLOWED`, so the SDK throws a `TypeError` first. With `selection`,
496
+ `totals.unfiltered` is how many PNRs the queue has before filtering (the host's
497
+ `1 MATCHING PNRS OUT OF 80`); with `queue` alone Sabre sends no `totals`. `additionalInfo` reads
498
+ `AdditionalQueueInfo` (a queue transferred elsewhere), typed from the design: no real response
499
+ has carried it yet.
500
+
501
+ ```ts
502
+ const { queues, totals } = await session.queue.count({ queue: 20, selection: [{ function: 'QD', criteria: '08NOV' }] });
503
+ // queues: [{ queue: 20, count: 1 }], totals: { unfiltered: 80 }
504
+ ```
479
505
  - **`activity`** returns, per queue, the eight counters of the host's `QA` screen: `numMessages`
480
506
  (on the queue now), `ended`, `ignored`, `moved`, `removed`, `transferred`,
481
507
  `leftMessageToContact` and `unableToReach`, plus `totals` for the PCC. Sabre also lists empty
482
508
  queues that have counters. No source says since when the counters run, so the SDK returns them
483
509
  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.
510
+ it does not need duty code 9. It takes the same `queue` or `range` as `count` (not both): with
511
+ `queue`, only that queue and no `totals`; with `range`, every queue in the range, even those
512
+ with all counters at 0, and the range's `totals`.
485
513
  - **Every queue result carries `respondedAt`**: Sabre's `eb:Timestamp`, in ISO 8601 UTC
486
514
  (`'2026-11-06T17:34:16Z'`). Unlike `countedAt` and `placedAt`, which come as the host sends them
487
515
  (no year, PCC local time), it has a year and a zone. It is missing only if Sabre does not send it.
@@ -608,21 +636,44 @@ fake.openSessions; // 0: the session was closed
608
636
  - It answers like production: empty queues, the end of the queue, `place` moving the PNR, the
609
637
  required PIC, and an expired session with `fake.expireSessions()`. `fake.calls` lists what the
610
638
  SDK asked for.
611
- - **Not simulated**: PCCs (the queues are the same for any `pcc`) and `selection`, which gets an
612
- error instead of the whole queue, in `access` and in `list`.
639
+ - `queue.count` and `queue.activity` with `queue` or `range` answer like production: one queue
640
+ without `totals`, or every queue in the range, empty ones included.
641
+ - `selection` is simulated for `QD`, `AK`, `AL`, `PI` and `AC` (`QUEUE_FAKE_SELECTION_FUNCTIONS`),
642
+ in `access`, `list` and `count`, combined with AND:
643
+ - `QD` filters on `placedAt` (`'05NOV'` or `'5NOV'`);
644
+ - `AK` filters on `placedBy`, PCC plus sine (`'ABCDAAT'`);
645
+ - `AL` filters on the airline of the paragraph's air segments, and `AC` on their status (one
646
+ letter matches every status that starts with it);
647
+ - `PI` filters on the PIC `place` used, or else on the paragraph's PIC notice (`011 SEE REMARKS`).
648
+
649
+ `access` walks only the PNRs that match and ends with `END OF DISPLAY`, also on entering when
650
+ none match (an assumption of the fake), and `ignore`, `remove` and `place` skip the rest. `list`
651
+ returns only the matching `Line`s and, like Sabre, rejects any code other than `QD` or `AK`.
652
+ `count` with `queue` counts the matches and returns `totals.unfiltered`; with `range` or without
653
+ a queue it gives Sabre's errors. Any other code gets a host error marked as not simulated,
654
+ never the whole queue.
655
+ - **Not simulated**: PCCs (the queues are the same for any `pcc`) and the host's combination rules
656
+ for `selection`.
613
657
  - `queueFixtures` holds the anonymized responses recorded in production (`walk`, `emptyQueue`,
614
658
  `place`, …), as `{ operation, status, body }`. Each one has its own locator aliases (`walk`:
615
659
  `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`, `placeWithoutInstructionCodeInside`:
616
660
  `AAAAAG`–`AAAAAH`), and their signature lines keep their shape (`… <alias> H`).
617
661
  - `queueFakePnrs(fixture)` turns a recording into the fake's PNRs: those of `list` first, in
618
662
  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:
663
+ each one. You choose the queue, and you can mix fixtures. The PNRs of `queueFixtures` have no
664
+ `primaryPassenger`: `list` was recorded without `primaryPassenger: true` (`/LN`), so Sabre did
665
+ not send the name. If you need one, add it with a `map`:
620
666
 
621
667
  ```ts
622
668
  const fake = createQueueFake({
623
669
  queues: { '9': queueFakePnrs(queueFixtures.walk), '50': [] },
624
670
  pnrs: queueFakePnrs(queueFixtures.place),
625
671
  });
672
+
673
+ const withPassenger = queueFakePnrs(queueFixtures.walk).map((pnr, index) => ({
674
+ ...pnr,
675
+ primaryPassenger: `TEST/PASSENGER${index + 1}`,
676
+ }));
626
677
  ```
627
678
 
628
679
  `examples/queue-review.mjs` is a full example, and its test runs it against the fake.
@@ -641,6 +692,14 @@ anonymizer.maskParagraph(item.paragraph); // host-format lines
641
692
  if (anonymizer.leaks(safe).length > 0) throw new Error('real data left');
642
693
  ```
643
694
 
695
+ In a browser bundle, import it from `sabre-client/anonymize` instead. It has only
696
+ `createAnonymizer`, `isSafeHostLine` and their types, without the fake or `queueFixtures`, which
697
+ `sabre-client/testing` carries in a single module (about 230 KB) that bundlers cannot drop:
698
+
699
+ ```ts
700
+ import { isSafeHostLine } from 'sabre-client/anonymize';
701
+ ```
702
+
644
703
  - Each locator gets an alias (`AAAAAA`, `AAAAAB`…), the same one everywhere it appears, also in
645
704
  later texts passed to the same anonymizer. `PassengerName` becomes `PASAJERO/UNO`, `Group`
646
705
  becomes `GRUPO`, `AgentSine` becomes `AAA` (except `SABRE`), `PseudoCityCode` becomes `XXXX`,
@@ -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":[]}