sabre-client 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -1207,12 +1207,20 @@ function resolveConfig(config) {
1207
1207
  if (environment !== "sandbox" && environment !== "production") {
1208
1208
  fail(`environment debe ser 'sandbox' o 'production' (recibido: ${JSON.stringify(environment)})`);
1209
1209
  }
1210
- const authVersion = config.authVersion ?? "v3";
1211
- if (authVersion !== "v3" && authVersion !== "v2") {
1212
- fail(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(authVersion)})`);
1210
+ const configVersion = config.authVersion ?? void 0;
1211
+ if (configVersion !== void 0 && configVersion !== "v3" && configVersion !== "v2") {
1212
+ fail(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(configVersion)})`);
1213
1213
  }
1214
1214
  const { credentials } = config;
1215
1215
  if (!isObject3(credentials)) fail("credentials es requerido");
1216
+ const credentialsVersion = credentials.authVersion ?? void 0;
1217
+ if (credentialsVersion !== void 0 && credentialsVersion !== "v3" && credentialsVersion !== "v2") {
1218
+ fail(`credentials.authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(credentialsVersion)})`);
1219
+ }
1220
+ if (configVersion !== void 0 && credentialsVersion !== void 0 && configVersion !== credentialsVersion) {
1221
+ fail(`authVersion '${configVersion}' no coincide con credentials.authVersion '${credentialsVersion}'`);
1222
+ }
1223
+ const authVersion = configVersion ?? credentialsVersion ?? "v3";
1216
1224
  requireString(credentials.username, "credentials.username");
1217
1225
  requireString(credentials.password, "credentials.password");
1218
1226
  if (authVersion === "v3") {
@@ -1491,14 +1499,16 @@ var TRANSIENT_CODES = /* @__PURE__ */ new Set([
1491
1499
  var PREVIOUS_ENTRY_ACTIVE_RETRIES = 3;
1492
1500
  var PREVIOUS_ENTRY_ACTIVE_DELAY_MS = 1e3;
1493
1501
  var PREVIOUS_ENTRY_ACTIVE = /PREVIOUS ENTRY ACTIVE/i;
1494
- var parser = new XMLParser({
1502
+ var PARSER_OPTIONS = {
1495
1503
  ignoreAttributes: false,
1496
1504
  attributeNamePrefix: "@",
1497
1505
  removeNSPrefix: true,
1498
1506
  parseTagValue: false,
1499
1507
  parseAttributeValue: false,
1500
1508
  processEntities: true
1501
- });
1509
+ };
1510
+ var parser = new XMLParser(PARSER_OPTIONS);
1511
+ var untrimmedParser = new XMLParser({ ...PARSER_OPTIONS, trimValues: false });
1502
1512
  var SoapTransport = class {
1503
1513
  #config;
1504
1514
  #log;
@@ -1738,11 +1748,11 @@ function scrubToken(text, security) {
1738
1748
  const token = security.binarySecurityToken;
1739
1749
  return text.split(token).join(REDACTED).split(xmlEscape(token)).join(REDACTED);
1740
1750
  }
1741
- function parseEnvelope(text) {
1751
+ function parseEnvelope(text, xmlParser = parser) {
1742
1752
  if (text.trim() === "") return void 0;
1743
1753
  let parsed;
1744
1754
  try {
1745
- parsed = parser.parse(text);
1755
+ parsed = xmlParser.parse(text);
1746
1756
  } catch {
1747
1757
  return void 0;
1748
1758
  }
@@ -1807,6 +1817,10 @@ function retryReason(error, idempotent) {
1807
1817
  if (error.status === void 0 || TRANSIENT_STATUS.has(error.status)) return "transient";
1808
1818
  return void 0;
1809
1819
  }
1820
+ function untrimmedPayload(raw) {
1821
+ const document = parseEnvelope(raw, untrimmedParser);
1822
+ return document === void 0 ? void 0 : asNode(firstElement(document.Body)[1]);
1823
+ }
1810
1824
  function firstElement(node2) {
1811
1825
  for (const [key, value] of Object.entries(node2)) {
1812
1826
  if (!key.startsWith("@") && key !== "#text") return [key, Array.isArray(value) ? value[0] : value];
@@ -2511,11 +2525,63 @@ function validateSelection(selection) {
2511
2525
  });
2512
2526
  }
2513
2527
 
2528
+ // src/session/command.ts
2529
+ var SABRE_COMMAND_SERVICE = "sabreCommand";
2530
+ var NAMESPACE5 = "http://webservices.sabre.com/sabreXML/2011/10";
2531
+ var MAX_COMMAND_LENGTH = 1023;
2532
+ var DOUBLE_WIDTH = /* @__PURE__ */ new Set(["!", "%", "$", "&", "\xA5", "\xA7"]);
2533
+ function fail4(message) {
2534
+ throw new TypeError(`sabre-client: ${message}`);
2535
+ }
2536
+ function sabreCommandPayload(command) {
2537
+ return `<SabreCommandLLSRQ xmlns="${NAMESPACE5}" Version="2.0.0" ReturnHostCommand="true"><Request Output="SCREEN"><HostCommand>${xmlEscape(command)}</HostCommand></Request></SabreCommandLLSRQ>`;
2538
+ }
2539
+ function commandToken(command) {
2540
+ return (command.trim().split(/[¥‡\s]/u)[0] ?? "").slice(0, 4);
2541
+ }
2542
+ function sabreCommandLength(command) {
2543
+ let length = 0;
2544
+ for (const char of command) length += DOUBLE_WIDTH.has(char) ? 2 : 1;
2545
+ return length;
2546
+ }
2547
+ function sabreCommandRequest(command, options) {
2548
+ if (typeof command !== "string" || command.trim() === "") fail4("command es requerido y debe ser un string no vac\xEDo");
2549
+ const length = sabreCommandLength(command);
2550
+ if (length > MAX_COMMAND_LENGTH) {
2551
+ fail4(`command mide ${length} y el m\xE1ximo es ${MAX_COMMAND_LENGTH} (!, %, $, &, \xA5 y \xA7 cuentan 2); Sabre responder\xEDa REQUEST EXCEEDED MAXIMUM LENGTH`);
2552
+ }
2553
+ const { idempotent = false, ...requestOptions } = options;
2554
+ if (typeof idempotent !== "boolean") fail4("idempotent debe ser un boolean");
2555
+ return {
2556
+ service: SABRE_COMMAND_SERVICE,
2557
+ action: "SabreCommandLLSRQ",
2558
+ payload: sabreCommandPayload(command),
2559
+ idempotent,
2560
+ operation: "command",
2561
+ ...requestOptions
2562
+ };
2563
+ }
2564
+ function parseSabreCommandResponse(response) {
2565
+ if (response.payloadName !== "SabreCommandLLSRS") {
2566
+ throw new SabreGatewayError(`${SABRE_COMMAND_SERVICE}: respuesta no reconocida (${response.payloadName})`, {
2567
+ protocol: "soap",
2568
+ status: response.status,
2569
+ retryable: false,
2570
+ raw: response.raw
2571
+ });
2572
+ }
2573
+ return {
2574
+ text: textOf((untrimmedPayload(response.raw) ?? response.payload).Response) ?? "",
2575
+ warnings: response.warnings,
2576
+ messageId: response.messageId
2577
+ };
2578
+ }
2579
+
2514
2580
  // src/session/session.ts
2515
2581
  var OTA_NAMESPACE = "http://www.opentravel.org/OTA/2002/11";
2516
2582
  var SWS_NAMESPACE = "http://webservices.sabre.com";
2517
2583
  var sessionSend = /* @__PURE__ */ Symbol("sabre-client.session.send");
2518
- function fail4(message) {
2584
+ function fail5(message) {
2519
2585
  throw new TypeError(`sabre-client: ${message}`);
2520
2586
  }
2521
2587
  var SESSION_ERROR_KINDS = /* @__PURE__ */ new Map([
@@ -2587,6 +2653,18 @@ var SabreSession = class {
2587
2653
  async ping(options = {}) {
2588
2654
  checkPingResponse(await this[sessionSend]({ ...pingRequest(), ...options }));
2589
2655
  }
2656
+ /**
2657
+ * Manda un comando de host (`SabreCommandLLSRQ` 2.0.0) y devuelve la pantalla como texto, sin
2658
+ * interpretarla. Es una vía de escape: sin tipos ni garantías, y un comando puede cambiar la
2659
+ * configuración del PCC o emitir. El comando va tal cual; el separador de Sabre Red (`‡`) se
2660
+ * escribe `¥`. Sin reintento salvo `{ idempotent: true }`, para comandos de solo lectura. La
2661
+ * pantalla llega sin recortar, y más de 1023 caracteres (contados como Sabre) lanza sin red.
2662
+ */
2663
+ async command(command, options = {}) {
2664
+ const request = sabreCommandRequest(command, options);
2665
+ this.#transport.log.emit("debug", "sabre.session.command", { command: commandToken(command) });
2666
+ return parseSabreCommandResponse(await this[sessionSend](request));
2667
+ }
2590
2668
  /**
2591
2669
  * Cierra la sesión con `SessionCloseRQ`: Sabre descarta el AAA e invalida el token. Es
2592
2670
  * idempotente: las llamadas siguientes devuelven la misma promesa, sin volver a la red. Si el
@@ -2614,11 +2692,11 @@ var SabreSession = class {
2614
2692
  return this.#send(request);
2615
2693
  }
2616
2694
  async #send(request) {
2617
- if (this.#closing !== void 0) fail4("session closed");
2695
+ if (this.#closing !== void 0) fail5("session closed");
2618
2696
  if (this.#unusable !== void 0) {
2619
2697
  throw new TypeError(`sabre-client: session unusable (${this.#unusable.kind})`, { cause: this.#unusable });
2620
2698
  }
2621
- if (this.#busy) fail4("session busy");
2699
+ if (this.#busy) fail5("session busy");
2622
2700
  this.#busy = true;
2623
2701
  try {
2624
2702
  const response = await this.#transport.request({ ...request, ...this.#envelopeFields() });
@@ -2773,7 +2851,7 @@ function usernameToken(credentials) {
2773
2851
  }
2774
2852
  async function openSession(transport, credentials, options = {}) {
2775
2853
  const { keepAlive = false, ...requestOptions } = options;
2776
- if (typeof keepAlive !== "boolean") fail4("keepAlive debe ser un boolean");
2854
+ if (typeof keepAlive !== "boolean") fail5("keepAlive debe ser un boolean");
2777
2855
  const conversationId = globalThis.crypto.randomUUID();
2778
2856
  const request = {
2779
2857
  service: "sessionCreate",
@@ -2876,6 +2954,39 @@ var SabreLegacy = class {
2876
2954
  }
2877
2955
  };
2878
2956
 
2957
+ // src/auth/credentials.ts
2958
+ var DEFAULT_DOMAIN = "AA";
2959
+ function fail6(message) {
2960
+ throw new TypeError(`sabre-client: ${message}`);
2961
+ }
2962
+ function requireString2(value, field) {
2963
+ if (typeof value !== "string" || value.trim() === "") fail6(`${field} es requerido y debe ser un string no vac\xEDo`);
2964
+ }
2965
+ function buildCredentials(options) {
2966
+ if (typeof options !== "object" || options === null) fail6("buildCredentials recibe un objeto");
2967
+ const authVersion = options.authVersion ?? "v3";
2968
+ if (authVersion !== "v3" && authVersion !== "v2") {
2969
+ fail6(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(authVersion)})`);
2970
+ }
2971
+ const { user, pcc, password, clientId, clientSecret } = options;
2972
+ const domain = options.domain ?? DEFAULT_DOMAIN;
2973
+ requireString2(user, "user");
2974
+ requireString2(password, "password");
2975
+ if (!isPseudoCityCode(pcc)) fail6("pcc debe tener 3 o 4 caracteres alfanum\xE9ricos");
2976
+ requireString2(domain, "domain");
2977
+ if (/[-:]/.test(domain)) fail6("domain no puede contener '-' ni ':'");
2978
+ if (clientId === void 0 !== (clientSecret === void 0)) {
2979
+ fail6("clientId y clientSecret van juntos: pasa los dos o ninguno");
2980
+ }
2981
+ if (authVersion === "v3" || clientId !== void 0) {
2982
+ requireString2(clientId, authVersion === "v3" ? "clientId (requerido con authVersion v3)" : "clientId");
2983
+ requireString2(clientSecret, authVersion === "v3" ? "clientSecret (requerido con authVersion v3)" : "clientSecret");
2984
+ }
2985
+ const username = authVersion === "v3" ? `${user}-${pcc}-${domain}` : `V1:${user}:${pcc}:${domain}`;
2986
+ if (authVersion === "v3") return { authVersion, clientId, clientSecret, username, password };
2987
+ return clientId === void 0 ? { authVersion, username, password } : { authVersion, clientId, clientSecret, username, password };
2988
+ }
2989
+
2879
2990
  // src/queue/host-date.ts
2880
2991
  var MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
2881
2992
  var DAY_MS = 864e5;
@@ -3258,7 +3369,7 @@ function fareComponent(fareComponentId, component) {
3258
3369
  }
3259
3370
 
3260
3371
  // src/index.ts
3261
- var VERSION = "0.3.0";
3372
+ var VERSION = "0.4.1";
3262
3373
  export {
3263
3374
  Sabre,
3264
3375
  SabreBusinessError,
@@ -3270,6 +3381,7 @@ export {
3270
3381
  SabreWarning,
3271
3382
  VERSION,
3272
3383
  buildBookingFlightDetails,
3384
+ buildCredentials,
3273
3385
  buildRevalidateItineraryRequest,
3274
3386
  buildSeatAvailabilityRequest,
3275
3387
  parseQueueParagraph,