@asteby/storefront 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -415,8 +415,10 @@ npx @asteby/storefront connect --ops https://ops.example.com --env-file .env --t
415
415
  # Conectada ✓ · 7 alcances · sk_…a91f
416
416
  ```
417
417
 
418
- `connect` redeems the code (`POST {ops}/api/connect/v1/redeem`), writes the key ATOMICALLY (temp file + rename, mode
419
- `0600`; default `./storefront_token`) with its non-secret state beside it (`<file>.json`), leaves ONLY `OPS_URL` and
418
+ `connect` redeems the code (`POST {ops}/api/connect/v1/redeem` with `{code, client:{name,version,runtime,hostname}}`; OPS
419
+ answers `{success:true,data:{token, connection, scopes, rotation_days, api_base}}`, an invalid, expired or used code is one
420
+ answer: `400 invalid_code`), writes the key ATOMICALLY (temp file + rename, mode `0600`; default `./storefront_token`)
421
+ with its non-secret state beside it (`<file>.json`: connection, `api_base`, `rotation_days`, `rotate_after`), leaves ONLY `OPS_URL` and
420
422
  `STOREFRONT_TOKEN_FILE` in the env file (never the key), prints just the prefix and last four characters, and checks
421
423
  the key with `token.introspect()`. Exit code is `1` with an es-MX message for an expired or used code, an unreachable
422
424
  OPS or a non-TTY stdin.
@@ -434,7 +436,7 @@ const storefront = createStorefrontClient({
434
436
  token: storefrontTokenFromEnv()!,
435
437
  })
436
438
  void diagnose(storefront) // one warning if scopes are missing; never throws, never delays boot
437
- connectStore().keepFresh() // heartbeat + rotation (POST /api/connect/v1/rotate) when rotate_after passes
439
+ connectStore().keepFresh() // heartbeat; rotates (POST /api/connect/v1/rotate) on rotate_now / next_rotation_at; honors Retry-After on 429
438
440
  ```
439
441
 
440
442
  `token` also accepts any `() => string`, called on every request. `diagnose` logs, at most once, which scopes are
package/dist/cli.js CHANGED
@@ -271,7 +271,11 @@ function createStorefrontClient(config) {
271
271
  * What the token in use is and can do (`GET {baseUrl}/_token`, a gateway route, not a `/storefront` one).
272
272
  * Needs no scope, so it answers even when the token is under-provisioned: `missing_scopes` says what to repair.
273
273
  */
274
- introspect: (options) => get("/_token", void 0, options)
274
+ introspect: async (options) => {
275
+ const body = await get("/_token", void 0, options);
276
+ const env = body;
277
+ return typeof env?.success === "boolean" && env.data ? env.data : body;
278
+ }
275
279
  },
276
280
  /** Returns a new client that sends the given buyer session token. */
277
281
  withCustomerToken(customerToken) {
@@ -460,7 +464,7 @@ function repairPath(connectionName) {
460
464
  import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from "fs";
461
465
  import { dirname, join, resolve } from "path";
462
466
  import { hostname } from "os";
463
- var SDK_VERSION = true ? "0.11.0" : "dev";
467
+ var SDK_VERSION = true ? "0.11.1" : "dev";
464
468
  function writeFileAtomic(file, content, mode = 384) {
465
469
  const dir = dirname(file);
466
470
  mkdirSync(dir, { recursive: true });
@@ -489,17 +493,26 @@ function writeFileAtomic(file, content, mode = 384) {
489
493
  throw err;
490
494
  }
491
495
  }
496
+ function connectClientInfo() {
497
+ return { name: "@asteby/storefront", version: SDK_VERSION, runtime: `node ${process.version}`, hostname: hostname() };
498
+ }
492
499
  var ConnectError = class extends Error {
493
- constructor(kind, message, status = 0) {
500
+ constructor(kind, message, status = 0, retryAfterSeconds) {
494
501
  super(message);
495
502
  this.kind = kind;
496
503
  this.status = status;
504
+ this.retryAfterSeconds = retryAfterSeconds;
497
505
  }
498
506
  kind;
499
507
  status;
508
+ retryAfterSeconds;
500
509
  name = "ConnectError";
501
510
  };
502
511
  var trimSlash = (u) => u.replace(/\/+$/, "");
512
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
513
+ function unwrapEnvelope(body) {
514
+ return isObj(body) && typeof body.success === "boolean" && "data" in body ? body.data : body;
515
+ }
503
516
  async function post(fetchImpl, url, body, bearer) {
504
517
  let res;
505
518
  try {
@@ -513,48 +526,70 @@ async function post(fetchImpl, url, body, bearer) {
513
526
  throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
514
527
  }
515
528
  const text = await res.text().catch(() => "");
516
- if (!res.ok) {
517
- if (res.status === 429) throw new ConnectError("rate_limited", "Demasiados intentos. Espera unos minutos e int\xE9ntalo de nuevo.", 429);
518
- if (res.status === 401 || res.status === 403) throw new ConnectError("unauthorized", "OPS rechaz\xF3 la credencial de esta tienda.", res.status);
519
- if (res.status >= 500) throw new ConnectError("server", "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
520
- throw new ConnectError("invalid_code", "El c\xF3digo no es v\xE1lido, ya se us\xF3 o venci\xF3. Genera uno nuevo en OPS \u2192 Conexiones y API.", res.status);
521
- }
522
- if (!text) return void 0;
529
+ let json;
523
530
  try {
524
- return JSON.parse(text);
531
+ json = text ? JSON.parse(text) : void 0;
525
532
  } catch {
526
- throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
533
+ json = void 0;
527
534
  }
535
+ if (!res.ok) {
536
+ const o = isObj(json) ? json : {};
537
+ const e = isObj(o.error) ? o.error : void 0;
538
+ const code = typeof o.error === "string" ? o.error : typeof e?.code === "string" ? e.code : "";
539
+ const message = typeof o.message === "string" ? o.message : typeof e?.message === "string" ? e.message : "";
540
+ if (res.status === 429) {
541
+ const ra = Number(res.headers.get("Retry-After"));
542
+ throw new ConnectError("rate_limited", message || "Demasiados intentos. Espera un momento y vuelve a intentarlo.", 429, Number.isFinite(ra) && ra > 0 ? ra : void 0);
543
+ }
544
+ if (res.status === 401 || code === "invalid_token") throw new ConnectError("unauthorized", message || "Clave inv\xE1lida.", res.status);
545
+ if (code === "invalid_code") throw new ConnectError("invalid_code", message || "El c\xF3digo no es v\xE1lido o ya venci\xF3. Genera uno nuevo desde OPS.", res.status);
546
+ if (res.status >= 500) throw new ConnectError("server", message || "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
547
+ throw new ConnectError("server", message || `OPS respondi\xF3 ${res.status}.`, res.status);
548
+ }
549
+ if (json === void 0) throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
550
+ return unwrapEnvelope(json);
528
551
  }
529
- function assertRedeem(r) {
552
+ function assertGrant(r) {
530
553
  const o = r;
531
- if (!o || typeof o.secret_token !== "string" || !o.secret_token || typeof o.connection_id !== "string" || !Array.isArray(o.scopes)) {
554
+ if (!isObj(o) || typeof o.token !== "string" || !o.token || !isObj(o.connection) || typeof o.connection.id !== "string" || !Array.isArray(o.scopes)) {
532
555
  throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
533
556
  }
534
557
  }
535
558
  async function redeemConnectionCode(opts) {
536
559
  const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
537
- const client_info = { sdk_version: SDK_VERSION, hostname: hostname(), ...opts.siteUrl ? { site_url: opts.siteUrl } : {} };
538
- const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client_info });
539
- assertRedeem(res);
560
+ const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client: connectClientInfo() });
561
+ assertGrant(res);
540
562
  return res;
541
563
  }
542
564
  var stateFileFor = (tokenFile) => `${tokenFile}.json`;
543
- function saveConnection(tokenFile, r, opsUrl = r.ops_url) {
544
- const state = { ops_url: trimSlash(r.ops_url || opsUrl), connection_id: r.connection_id, environment: r.environment, scopes: r.scopes, rotate_after: r.rotate_after };
565
+ var DEFAULT_ROTATION_DAYS = 90;
566
+ var DAY_MS = 864e5;
567
+ function stateFromGrant(g, opsUrl, prev, now = Date.now()) {
568
+ const days = g.rotation_days && g.rotation_days > 0 ? g.rotation_days : prev?.rotation_days ?? DEFAULT_ROTATION_DAYS;
569
+ return {
570
+ ops_url: trimSlash(opsUrl),
571
+ api_base: g.api_base || prev?.api_base || `${trimSlash(opsUrl)}/api/gw`,
572
+ connection: g.connection,
573
+ scopes: g.scopes,
574
+ rotation_days: days,
575
+ rotate_after: new Date(now + days * DAY_MS).toISOString(),
576
+ ...g.grace_until ? { grace_until: g.grace_until } : {}
577
+ };
578
+ }
579
+ function saveConnection(tokenFile, g, state) {
545
580
  writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
546
- writeFileAtomic(tokenFile, r.secret_token + "\n");
581
+ writeFileAtomic(tokenFile, g.token + "\n");
547
582
  }
548
583
 
549
584
  // src/cli.ts
550
- var HELP = `Uso: npx @asteby/storefront connect --ops <url> [--env-file <ruta>] [--token-file <ruta>] [--site <url>]
585
+ var HELP = `Uso: npx @asteby/storefront connect --ops <url> [--env-file <ruta>] [--token-file <ruta>]
551
586
 
552
587
  Conecta esta tienda con OPS. El c\xF3digo de conexi\xF3n (K7QM-2XPA) se pide en la terminal; nunca va como argumento.
553
588
 
554
589
  --ops Direcci\xF3n de OPS (https://ops.ejemplo.com). Tambi\xE9n OPS_URL.
555
590
  --token-file D\xF3nde guardar la clave (por omisi\xF3n ./storefront_token). Modo 0600, escritura at\xF3mica.
556
591
  --env-file Archivo .env donde dejar SOLO OPS_URL y STOREFRONT_TOKEN_FILE (nunca la clave).
557
- --site Direcci\xF3n p\xFAblica de la tienda (opcional, informativa).`;
592
+ `;
558
593
  function parseArgs(argv) {
559
594
  const flags = {};
560
595
  const [cmd, ...rest] = argv;
@@ -562,7 +597,7 @@ function parseArgs(argv) {
562
597
  const a = rest[i];
563
598
  const eq = a.indexOf("=");
564
599
  const key = (eq > 0 ? a.slice(0, eq) : a).replace(/^--/, "");
565
- if (!a.startsWith("--") || !["ops", "env-file", "token-file", "site", "code"].includes(key)) return { cmd, flags, bad: a };
600
+ if (!a.startsWith("--") || !["ops", "env-file", "token-file", "code"].includes(key)) return { cmd, flags, bad: a };
566
601
  if (key === "code") return { cmd, flags, bad: "--code (el c\xF3digo se pide en la terminal, no como argumento)" };
567
602
  const val = eq > 0 ? a.slice(eq + 1) : rest[++i];
568
603
  if (!val) return { cmd, flags, bad: a };
@@ -616,25 +651,25 @@ ${HELP}`);
616
651
  return 1;
617
652
  }
618
653
  try {
619
- const r = await redeemConnectionCode({ opsUrl, code, ...flags.site ? { siteUrl: flags.site } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
620
- saveConnection(tokenFile, r, opsUrl);
654
+ const r = await redeemConnectionCode({ opsUrl, code, ...io.fetch ? { fetch: io.fetch } : {} });
655
+ saveConnection(tokenFile, r, stateFromGrant(r, opsUrl, null));
621
656
  if (envFile) {
622
657
  const current = existsSync2(envFile) ? readFileSync2(envFile, "utf8") : "";
623
658
  if (/^\s*(export\s+)?STOREFRONT_TOKEN\s*=/m.test(current)) {
624
659
  io.stderr("Aviso: tu .env a\xFAn tiene STOREFRONT_TOKEN con una clave anterior; b\xF3rrala, el archivo de clave tiene prioridad.");
625
660
  }
626
- writeFileAtomic(envFile, upsertEnv(current, { OPS_URL: r.ops_url || opsUrl, STOREFRONT_TOKEN_FILE: tokenFile }), 384);
661
+ writeFileAtomic(envFile, upsertEnv(current, { OPS_URL: opsUrl, STOREFRONT_TOKEN_FILE: tokenFile }), 384);
627
662
  }
628
- io.stdout(`Conectada \u2713 \xB7 ${r.scopes.length} alcances \xB7 ${maskToken(r.secret_token)}`);
663
+ io.stdout(`Conectada \u2713 \xB7 ${r.scopes.length} alcances \xB7 ${maskToken(r.token)}`);
629
664
  io.stdout(`Clave guardada en ${tokenFile} (modo 0600).${envFile ? ` Variables en ${envFile}.` : ` Usa STOREFRONT_TOKEN_FILE=${tokenFile}.`}`);
630
- const base = `${(r.ops_url || opsUrl).replace(/\/+$/, "")}/api/gw`;
631
- const sf = createStorefrontClient({ baseUrl: base, token: r.secret_token, ...io.fetch ? { fetch: io.fetch } : {}, timeoutMs: 1e4 });
665
+ const base = r.api_base || `${opsUrl}/api/gw`;
666
+ const sf = createStorefrontClient({ baseUrl: base, token: r.token, ...io.fetch ? { fetch: io.fetch } : {}, timeoutMs: 1e4 });
632
667
  try {
633
668
  const info = await sf.token.introspect();
634
669
  if (info.missing_scopes?.length) {
635
670
  io.stderr(`Aviso: a la clave le faltan permisos (${info.missing_scopes.join(", ")}). Rep\xE1ralo en ${repairPath()}.`);
636
671
  } else {
637
- io.stdout(`Verificada con OPS \xB7 entorno ${info.environment}.`);
672
+ io.stdout(`Verificada con OPS \xB7 entorno ${info.environment ?? r.connection.environment}.`);
638
673
  }
639
674
  } catch {
640
675
  io.stderr("Aviso: la clave se guard\xF3 pero no pudimos verificarla con OPS todav\xEDa.");
@@ -1,5 +1,5 @@
1
- import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-C-Ley-Mt.cjs';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-C-Ley-Mt.cjs';
1
+ import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-bsdlAumU.cjs';
2
+ export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-bsdlAumU.cjs';
3
3
  import './geo.cjs';
4
4
 
5
5
  /** A value read from a document: text, or the codes of a catalog (`multiple` fields). */
@@ -1,5 +1,5 @@
1
- import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-D7UJxrba.js';
2
- export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-D7UJxrba.js';
1
+ import { U as FiscalDocumentSource, aR as RegionFiscalEntry, Z as IdentityDocumentSpec, T as FiscalDocumentEvidence, aS as RegionFiscalId, $ as IdentityRulesV1 } from './types-B1E3GySa.js';
2
+ export { Y as IdentityCapture, _ as IdentityFieldRule, a0 as IdentityStrategy } from './types-B1E3GySa.js';
3
3
  import './geo.js';
4
4
 
5
5
  /** A value read from a document: text, or the codes of a catalog (`multiple` fields). */
package/dist/index.cjs CHANGED
@@ -303,7 +303,11 @@ function createStorefrontClient(config) {
303
303
  * What the token in use is and can do (`GET {baseUrl}/_token`, a gateway route, not a `/storefront` one).
304
304
  * Needs no scope, so it answers even when the token is under-provisioned: `missing_scopes` says what to repair.
305
305
  */
306
- introspect: (options) => get("/_token", void 0, options)
306
+ introspect: async (options) => {
307
+ const body = await get("/_token", void 0, options);
308
+ const env = body;
309
+ return typeof env?.success === "boolean" && env.data ? env.data : body;
310
+ }
307
311
  },
308
312
  /** Returns a new client that sends the given buyer session token. */
309
313
  withCustomerToken(customerToken) {