@asteby/storefront 0.11.0 → 0.11.2

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/node.js CHANGED
@@ -11,7 +11,7 @@ function maskToken(token) {
11
11
  }
12
12
 
13
13
  // src/node.ts
14
- var SDK_VERSION = true ? "0.11.0" : "dev";
14
+ var SDK_VERSION = true ? "0.11.2" : "dev";
15
15
  function createFileTokenSource(file, options = {}) {
16
16
  const ttl = options.ttlMs ?? 3e4;
17
17
  const now = options.now ?? Date.now;
@@ -78,17 +78,26 @@ function writeFileAtomic(file, content, mode = 384) {
78
78
  throw err;
79
79
  }
80
80
  }
81
+ function connectClientInfo() {
82
+ return { name: "@asteby/storefront", version: SDK_VERSION, runtime: `node ${process.version}`, hostname: hostname() };
83
+ }
81
84
  var ConnectError = class extends Error {
82
- constructor(kind, message, status = 0) {
85
+ constructor(kind, message, status = 0, retryAfterSeconds) {
83
86
  super(message);
84
87
  this.kind = kind;
85
88
  this.status = status;
89
+ this.retryAfterSeconds = retryAfterSeconds;
86
90
  }
87
91
  kind;
88
92
  status;
93
+ retryAfterSeconds;
89
94
  name = "ConnectError";
90
95
  };
91
96
  var trimSlash = (u) => u.replace(/\/+$/, "");
97
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
98
+ function unwrapEnvelope(body) {
99
+ return isObj(body) && typeof body.success === "boolean" && "data" in body ? body.data : body;
100
+ }
92
101
  async function post(fetchImpl, url, body, bearer) {
93
102
  let res;
94
103
  try {
@@ -102,37 +111,59 @@ async function post(fetchImpl, url, body, bearer) {
102
111
  throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
103
112
  }
104
113
  const text = await res.text().catch(() => "");
105
- if (!res.ok) {
106
- if (res.status === 429) throw new ConnectError("rate_limited", "Demasiados intentos. Espera unos minutos e int\xE9ntalo de nuevo.", 429);
107
- if (res.status === 401 || res.status === 403) throw new ConnectError("unauthorized", "OPS rechaz\xF3 la credencial de esta tienda.", res.status);
108
- if (res.status >= 500) throw new ConnectError("server", "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
109
- 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);
110
- }
111
- if (!text) return void 0;
114
+ let json;
112
115
  try {
113
- return JSON.parse(text);
116
+ json = text ? JSON.parse(text) : void 0;
114
117
  } catch {
115
- throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
118
+ json = void 0;
116
119
  }
120
+ if (!res.ok) {
121
+ const o = isObj(json) ? json : {};
122
+ const e = isObj(o.error) ? o.error : void 0;
123
+ const code = typeof o.error === "string" ? o.error : typeof e?.code === "string" ? e.code : "";
124
+ const message = typeof o.message === "string" ? o.message : typeof e?.message === "string" ? e.message : "";
125
+ if (res.status === 429) {
126
+ const ra = Number(res.headers.get("Retry-After"));
127
+ throw new ConnectError("rate_limited", message || "Demasiados intentos. Espera un momento y vuelve a intentarlo.", 429, Number.isFinite(ra) && ra > 0 ? ra : void 0);
128
+ }
129
+ if (res.status === 401 || code === "invalid_token") throw new ConnectError("unauthorized", message || "Clave inv\xE1lida.", res.status);
130
+ 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);
131
+ if (res.status >= 500) throw new ConnectError("server", message || "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
132
+ throw new ConnectError("server", message || `OPS respondi\xF3 ${res.status}.`, res.status);
133
+ }
134
+ if (json === void 0) throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
135
+ return unwrapEnvelope(json);
117
136
  }
118
- function assertRedeem(r) {
137
+ function assertGrant(r) {
119
138
  const o = r;
120
- if (!o || typeof o.secret_token !== "string" || !o.secret_token || typeof o.connection_id !== "string" || !Array.isArray(o.scopes)) {
139
+ if (!isObj(o) || typeof o.token !== "string" || !o.token || !isObj(o.connection) || typeof o.connection.id !== "string" || !Array.isArray(o.scopes)) {
121
140
  throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
122
141
  }
123
142
  }
124
143
  async function redeemConnectionCode(opts) {
125
144
  const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
126
- const client_info = { sdk_version: SDK_VERSION, hostname: hostname(), ...opts.siteUrl ? { site_url: opts.siteUrl } : {} };
127
- const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client_info });
128
- assertRedeem(res);
145
+ const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client: connectClientInfo() });
146
+ assertGrant(res);
129
147
  return res;
130
148
  }
131
149
  var stateFileFor = (tokenFile) => `${tokenFile}.json`;
132
- function saveConnection(tokenFile, r, opsUrl = r.ops_url) {
133
- 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 };
150
+ var DEFAULT_ROTATION_DAYS = 90;
151
+ var DAY_MS = 864e5;
152
+ function stateFromGrant(g, opsUrl, prev, now = Date.now()) {
153
+ const days = g.rotation_days && g.rotation_days > 0 ? g.rotation_days : prev?.rotation_days ?? DEFAULT_ROTATION_DAYS;
154
+ return {
155
+ ops_url: trimSlash(opsUrl),
156
+ api_base: g.api_base || prev?.api_base || `${trimSlash(opsUrl)}/api/gw`,
157
+ connection: g.connection,
158
+ scopes: g.scopes,
159
+ rotation_days: days,
160
+ rotate_after: new Date(now + days * DAY_MS).toISOString(),
161
+ ...g.grace_until ? { grace_until: g.grace_until } : {}
162
+ };
163
+ }
164
+ function saveConnection(tokenFile, g, state) {
134
165
  writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
135
- writeFileAtomic(tokenFile, r.secret_token + "\n");
166
+ writeFileAtomic(tokenFile, g.token + "\n");
136
167
  }
137
168
  function loadConnectionState(tokenFile) {
138
169
  try {
@@ -145,6 +176,7 @@ function connectStore(options = {}) {
145
176
  const tokenFile = resolve(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? "storefront_token");
146
177
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
147
178
  const now = options.now ?? Date.now;
179
+ let retryAfterMs = 0;
148
180
  const report = (m) => {
149
181
  try {
150
182
  options.onError?.(m);
@@ -152,31 +184,37 @@ function connectStore(options = {}) {
152
184
  }
153
185
  };
154
186
  async function tick() {
155
- const state = loadConnectionState(tokenFile);
187
+ retryAfterMs = 0;
188
+ let state = loadConnectionState(tokenFile);
156
189
  const opsUrl = options.opsUrl ?? state?.ops_url;
157
190
  let token;
158
191
  try {
159
192
  token = readFileSync(tokenFile, "utf8").trim();
160
193
  } catch {
161
- report(`No se pudo leer el archivo del token (${tokenFile}).`);
194
+ report(`No se pudo leer el archivo de la clave (${tokenFile}).`);
162
195
  return "failed";
163
196
  }
164
197
  if (!opsUrl || !token) {
165
- report("Falta la direcci\xF3n de OPS o el token: vuelve a ejecutar `npx @asteby/storefront connect`.");
198
+ report("Falta la direcci\xF3n de OPS o la clave: vuelve a ejecutar `npx @asteby/storefront connect`.");
166
199
  return "failed";
167
200
  }
168
201
  const base = `${trimSlash(opsUrl)}/api/connect/v1`;
169
202
  try {
170
- await post(fetchImpl, `${base}/heartbeat`, { sdk_version: SDK_VERSION, hostname: hostname() }, token);
171
- const due = state?.rotate_after ? Date.parse(state.rotate_after) : NaN;
172
- if (!Number.isNaN(due) && now() >= due) {
203
+ const hb = await post(fetchImpl, `${base}/heartbeat`, { client: connectClientInfo() }, token);
204
+ if (state && hb?.next_rotation_at && hb.next_rotation_at !== state.rotate_after) {
205
+ state = { ...state, rotate_after: hb.next_rotation_at };
206
+ writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
207
+ }
208
+ const due = Date.parse(hb?.next_rotation_at ?? state?.rotate_after ?? "");
209
+ if (hb?.rotate_now === true || !Number.isNaN(due) && now() >= due) {
173
210
  const next = await post(fetchImpl, `${base}/rotate`, {}, token);
174
- assertRedeem(next);
175
- saveConnection(tokenFile, { ...next, ops_url: next.ops_url || opsUrl }, opsUrl);
211
+ assertGrant(next);
212
+ saveConnection(tokenFile, next, stateFromGrant(next, opsUrl, state, now()));
176
213
  return "rotated";
177
214
  }
178
215
  return "ok";
179
216
  } catch (err) {
217
+ if (err instanceof ConnectError && err.retryAfterSeconds) retryAfterMs = err.retryAfterSeconds * 1e3;
180
218
  report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Fall\xF3 el latido de la conexi\xF3n (${maskToken(token)}).`);
181
219
  return "failed";
182
220
  }
@@ -187,23 +225,36 @@ function connectStore(options = {}) {
187
225
  tick,
188
226
  /** Starts the periodic heartbeat; returns `stop()`. */
189
227
  keepFresh(opts = {}) {
190
- const timer = setInterval(() => void tick(), opts.intervalMs ?? 6e5);
191
- timer.unref?.();
192
- void tick();
193
- return () => clearInterval(timer);
228
+ const interval = opts.intervalMs ?? 6e5;
229
+ let stopped = false;
230
+ let timer;
231
+ const loop = async () => {
232
+ await tick();
233
+ if (stopped) return;
234
+ timer = setTimeout(() => void loop(), Math.max(interval, retryAfterMs));
235
+ timer.unref?.();
236
+ };
237
+ void loop();
238
+ return () => {
239
+ stopped = true;
240
+ if (timer) clearTimeout(timer);
241
+ };
194
242
  }
195
243
  };
196
244
  }
197
245
  export {
198
246
  ConnectError,
199
247
  SDK_VERSION,
248
+ connectClientInfo,
200
249
  connectStore,
201
250
  createFileTokenSource,
202
251
  loadConnectionState,
203
252
  redeemConnectionCode,
204
253
  saveConnection,
205
254
  stateFileFor,
255
+ stateFromGrant,
206
256
  storefrontTokenFromEnv,
257
+ unwrapEnvelope,
207
258
  writeFileAtomic
208
259
  };
209
260
  //# sourceMappingURL=node.js.map
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node.ts","../src/diagnose.ts"],"sourcesContent":["/**\n * Node-only helpers (`@asteby/storefront/node`): read the store's token from a file that can rotate, and the\n * connection protocol with OPS (`connectStore`). Nothing here runs in browsers or edge runtimes.\n */\nimport { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { hostname } from 'node:os'\nimport { maskToken } from './diagnose.js'\nimport type { FetchLike } from './client.js'\n\ndeclare const __SDK_VERSION__: string | undefined\nexport const SDK_VERSION: string = typeof __SDK_VERSION__ === 'string' ? __SDK_VERSION__ : 'dev'\n\n// ---------------------------------------------------------------------------\n// Token from a file\n// ---------------------------------------------------------------------------\n\nexport type FileTokenSourceOptions = {\n /** How often the file is checked again (default 30 s). Between checks the cached token is returned. */\n ttlMs?: number\n /** Clock, for tests. */\n now?: () => number\n}\n\n/**\n * A `() => string` for `createStorefrontClient({ token })` that reads `file`, caches it and looks again every\n * `ttlMs` (30 s): the file is read only when its mtime or size changed. A failed re-read keeps the last good token\n * (a rotation in progress never takes the store down); only a first read that fails throws.\n */\nexport function createFileTokenSource(file: string, options: FileTokenSourceOptions = {}): () => string {\n const ttl = options.ttlMs ?? 30_000\n const now = options.now ?? Date.now\n let token = ''\n let stamp = ''\n let checkedAt = -Infinity\n return () => {\n if (token && now() - checkedAt < ttl) return token\n checkedAt = now()\n try {\n const st = statSync(file)\n const next = `${st.mtimeMs}:${st.size}`\n if (!token || next !== stamp) {\n const text = readFileSync(file, 'utf8').trim()\n if (text) {\n token = text\n stamp = next\n } else if (!token) {\n throw new Error('token file is empty')\n }\n }\n } catch (err) {\n if (!token) throw new Error(`Cannot read the storefront token file ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n return token\n }\n}\n\nexport type TokenEnv = Record<string, string | undefined>\n\n/**\n * The store's token from the environment, in this order:\n * 1. `STOREFRONT_TOKEN_FILE` (path),\n * 2. `$CREDENTIALS_DIRECTORY/storefront_token` (systemd `LoadCredential=`),\n * 3. `STOREFRONT_TOKEN` (the plain variable, as before).\n * Files come back as a rotating source (re-read every 30 s); the variable as a plain string. `undefined` when\n * nothing is configured.\n */\nexport function storefrontTokenFromEnv(env: TokenEnv = process.env, options: FileTokenSourceOptions = {}): string | (() => string) | undefined {\n const file = env.STOREFRONT_TOKEN_FILE?.trim()\n if (file) return createFileTokenSource(file, options)\n const dir = env.CREDENTIALS_DIRECTORY?.trim()\n if (dir) {\n const f = join(dir, 'storefront_token')\n if (existsSync(f)) return createFileTokenSource(f, options)\n }\n const plain = env.STOREFRONT_TOKEN?.trim()\n return plain || undefined\n}\n\n// ---------------------------------------------------------------------------\n// Atomic write\n// ---------------------------------------------------------------------------\n\n/** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */\nexport function writeFileAtomic(file: string, content: string, mode = 0o600): void {\n const dir = dirname(file)\n mkdirSync(dir, { recursive: true })\n const tmp = join(dir, `.${file.split('/').pop()}.${process.pid}.${Date.now().toString(36)}.tmp`)\n const fd = openSync(tmp, 'wx', mode)\n try {\n writeSync(fd, content)\n fsyncSync(fd)\n } catch (err) {\n closeSync(fd)\n try {\n unlinkSync(tmp)\n } catch {\n /* best effort */\n }\n throw err\n }\n closeSync(fd)\n try {\n chmodSync(tmp, mode) // umask can only remove bits at creation; make it exact\n renameSync(tmp, file)\n } catch (err) {\n try {\n unlinkSync(tmp)\n } catch {\n /* best effort */\n }\n throw err\n }\n}\n\n// ---------------------------------------------------------------------------\n// Connection protocol\n// ---------------------------------------------------------------------------\n\nexport type RedeemResponse = {\n ops_url: string\n connection_id: string\n environment: string\n secret_token: string\n publishable_token?: string\n scopes: string[]\n /** ISO time from which the token should be rotated. */\n rotate_after: string\n}\n\nexport type ConnectClientInfo = { sdk_version: string; hostname: string; site_url?: string }\n\n/** Why a connection call failed, in terms an operator can act on. */\nexport type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response'\n\nexport class ConnectError extends Error {\n override readonly name = 'ConnectError'\n constructor(\n readonly kind: ConnectErrorKind,\n message: string,\n readonly status = 0,\n ) {\n super(message)\n }\n}\n\nconst trimSlash = (u: string) => u.replace(/\\/+$/, '')\n\nasync function post<T>(fetchImpl: FetchLike, url: string, body: unknown, bearer?: string): Promise<T> {\n let res: Response\n try {\n res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}) },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new ConnectError('network', 'No pudimos comunicarnos con OPS. Revisa la dirección y tu conexión a internet.')\n }\n const text = await res.text().catch(() => '')\n if (!res.ok) {\n if (res.status === 429) throw new ConnectError('rate_limited', 'Demasiados intentos. Espera unos minutos e inténtalo de nuevo.', 429)\n if (res.status === 401 || res.status === 403) throw new ConnectError('unauthorized', 'OPS rechazó la credencial de esta tienda.', res.status)\n if (res.status >= 500) throw new ConnectError('server', 'OPS no pudo atender la solicitud. Inténtalo de nuevo en un momento.', res.status)\n throw new ConnectError('invalid_code', 'El código no es válido, ya se usó o venció. Genera uno nuevo en OPS → Conexiones y API.', res.status)\n }\n if (!text) return undefined as T\n try {\n return JSON.parse(text) as T\n } catch {\n throw new ConnectError('invalid_response', 'OPS respondió algo que no entendimos.', res.status)\n }\n}\n\nfunction assertRedeem(r: unknown): asserts r is RedeemResponse {\n const o = r as Partial<RedeemResponse> | undefined\n if (!o || typeof o.secret_token !== 'string' || !o.secret_token || typeof o.connection_id !== 'string' || !Array.isArray(o.scopes)) {\n throw new ConnectError('invalid_response', 'OPS respondió algo que no entendimos.')\n }\n}\n\nexport type RedeemOptions = { opsUrl: string; code: string; siteUrl?: string; fetch?: FetchLike }\n\n/** Exchanges the one-time connection code for the store's token. `POST {ops}/api/connect/v1/redeem`. */\nexport async function redeemConnectionCode(opts: RedeemOptions): Promise<RedeemResponse> {\n const fetchImpl = opts.fetch ?? (globalThis.fetch.bind(globalThis) as FetchLike)\n const client_info: ConnectClientInfo = { sdk_version: SDK_VERSION, hostname: hostname(), ...(opts.siteUrl ? { site_url: opts.siteUrl } : {}) }\n const res = await post<unknown>(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client_info })\n assertRedeem(res)\n return res\n}\n\n/** What is kept next to the token file: everything but the secret. */\nexport type ConnectionState = {\n ops_url: string\n connection_id: string\n environment: string\n scopes: string[]\n rotate_after: string\n}\n\nexport const stateFileFor = (tokenFile: string) => `${tokenFile}.json`\n\n/** Saves a redeemed/rotated connection: the token (0600, atomic) and its non-secret state beside it. */\nexport function saveConnection(tokenFile: string, r: RedeemResponse, opsUrl = r.ops_url): void {\n const state: ConnectionState = { ops_url: trimSlash(r.ops_url || opsUrl), connection_id: r.connection_id, environment: r.environment, scopes: r.scopes, rotate_after: r.rotate_after }\n // State first: if the process dies in between, the old token still works and rotation is just retried.\n writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + '\\n')\n writeFileAtomic(tokenFile, r.secret_token + '\\n')\n}\n\nexport function loadConnectionState(tokenFile: string): ConnectionState | null {\n try {\n return JSON.parse(readFileSync(stateFileFor(tokenFile), 'utf8')) as ConnectionState\n } catch {\n return null\n }\n}\n\nexport type ConnectStoreOptions = {\n /** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */\n opsUrl?: string\n /** File `connect` wrote (default: `STOREFRONT_TOKEN_FILE`, then `./storefront_token`). */\n tokenFile?: string\n fetch?: FetchLike\n /** Clock, for tests. */\n now?: () => number\n /** Called with a short message when a heartbeat or rotation fails. It never receives a token. */\n onError?: (message: string) => void\n}\n\nexport type KeepFreshOptions = {\n /** Heartbeat period (default 10 min). */\n intervalMs?: number\n}\n\n/**\n * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and, when `rotate_after`\n * passes, rotates the token (`POST /api/connect/v1/rotate`, Bearer = current `sk_`), writes the new file and lets a\n * `createFileTokenSource` client pick it up within its re-read interval. The old token keeps working during a grace\n * period set by OPS. It never throws and never keeps the process alive.\n */\nexport function connectStore(options: ConnectStoreOptions = {}) {\n const tokenFile = resolve(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? 'storefront_token')\n const fetchImpl = options.fetch ?? (globalThis.fetch.bind(globalThis) as FetchLike)\n const now = options.now ?? Date.now\n const report = (m: string) => {\n try {\n options.onError?.(m)\n } catch {\n /* ignore */\n }\n }\n\n async function tick(): Promise<'ok' | 'rotated' | 'failed'> {\n const state = loadConnectionState(tokenFile)\n const opsUrl = options.opsUrl ?? state?.ops_url\n let token: string\n try {\n token = readFileSync(tokenFile, 'utf8').trim()\n } catch {\n report(`No se pudo leer el archivo del token (${tokenFile}).`)\n return 'failed'\n }\n if (!opsUrl || !token) {\n report('Falta la dirección de OPS o el token: vuelve a ejecutar `npx @asteby/storefront connect`.')\n return 'failed'\n }\n const base = `${trimSlash(opsUrl)}/api/connect/v1`\n try {\n await post<unknown>(fetchImpl, `${base}/heartbeat`, { sdk_version: SDK_VERSION, hostname: hostname() }, token)\n const due = state?.rotate_after ? Date.parse(state.rotate_after) : NaN\n if (!Number.isNaN(due) && now() >= due) {\n const next = await post<unknown>(fetchImpl, `${base}/rotate`, {}, token)\n assertRedeem(next)\n saveConnection(tokenFile, { ...next, ops_url: next.ops_url || opsUrl }, opsUrl)\n return 'rotated'\n }\n return 'ok'\n } catch (err) {\n report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Falló el latido de la conexión (${maskToken(token)}).`)\n return 'failed'\n }\n }\n\n return {\n tokenFile,\n /** One heartbeat (+ rotation when due). Resolves, never rejects. */\n tick,\n /** Starts the periodic heartbeat; returns `stop()`. */\n keepFresh(opts: KeepFreshOptions = {}): () => void {\n const timer = setInterval(() => void tick(), opts.intervalMs ?? 600_000)\n timer.unref?.()\n void tick()\n return () => clearInterval(timer)\n },\n }\n}\n","import type { StorefrontClient } from './client.js'\nimport { isStorefrontError } from './errors.js'\nimport type { TokenIntrospection } from './types.js'\n\n/** `sk_…a91f`: the prefix and the last four characters, never enough to use the token. */\nexport function maskToken(token: string | null | undefined): string {\n if (!token) return '(sin token)'\n const prefix = /^[a-z]{2}_/.exec(token)?.[0] ?? ''\n return token.length <= prefix.length + 8 ? `${prefix}…` : `${prefix}…${token.slice(-4)}`\n}\n\nexport type DiagnoseOptions = {\n /** Where the warning goes (default `console.warn`). Receives ONE line per problem. */\n log?: (message: string) => void\n /** Name of the connection, shown in the repair path. */\n connectionName?: string\n /** Give up after this long (default 3000 ms). */\n timeoutMs?: number\n}\n\n/** Where the operator repairs a connection in OPS (es-MX, as the screen names it). */\nexport function repairPath(connectionName?: string): string {\n return `OPS → Conexiones y API → ${connectionName ?? '<conexión>'} → Reparar permisos`\n}\n\n/**\n * Startup check of the store's credential: asks `token.introspect()` and, when scopes are missing, logs ONE clear\n * warning with the repair path (never the token, only `sk_…a91f`). It NEVER throws and is meant to be fired and\n * forgotten, so it cannot delay or break the boot:\n *\n * ```ts\n * void diagnose(storefront)\n * ```\n *\n * Resolves with the introspection, or `null` when it could not be obtained (older OPS, outage, bad token).\n */\nexport async function diagnose(client: StorefrontClient, options: DiagnoseOptions = {}): Promise<TokenIntrospection | null> {\n const log = options.log ?? ((m: string) => console.warn(m))\n const timeoutMs = options.timeoutMs ?? 3000\n const raw = client.config.token\n let mask: string\n try {\n mask = maskToken(typeof raw === 'function' ? raw() : raw)\n } catch {\n mask = maskToken(null)\n }\n try {\n const info = await client.token.introspect({ signal: AbortSignal.timeout(timeoutMs) })\n const missing = Array.isArray(info?.missing_scopes) ? info.missing_scopes : []\n if (missing.length > 0) {\n log(\n `[@asteby/storefront] La credencial ${mask} no tiene permisos que la tienda necesita: ${missing.join(', ')}. ` +\n `Repáralo en ${repairPath(options.connectionName)} (no hace falta cambiar la clave ni reiniciar).`,\n )\n }\n return info ?? null\n } catch (err) {\n if (isStorefrontError(err) && err.status === 401) {\n try {\n log(`[@asteby/storefront] OPS rechazó la credencial ${mask} (401): está revocada o vencida. Vuelve a conectar la tienda en OPS → Conexiones y API.`)\n } catch {\n /* a broken logger must not break boot */\n }\n }\n return null\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/node.ts","../src/diagnose.ts"],"sourcesContent":["/**\n * Node-only helpers (`@asteby/storefront/node`): read the store's token from a file that can rotate, and the\n * connection protocol with OPS (`connectStore`). Nothing here runs in browsers or edge runtimes.\n */\nimport { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { hostname } from 'node:os'\nimport { maskToken } from './diagnose.js'\nimport type { FetchLike } from './client.js'\n\ndeclare const __SDK_VERSION__: string | undefined\nexport const SDK_VERSION: string = typeof __SDK_VERSION__ === 'string' ? __SDK_VERSION__ : 'dev'\n\n// ---------------------------------------------------------------------------\n// Token from a file\n// ---------------------------------------------------------------------------\n\nexport type FileTokenSourceOptions = {\n /** How often the file is checked again (default 30 s). Between checks the cached token is returned. */\n ttlMs?: number\n /** Clock, for tests. */\n now?: () => number\n}\n\n/**\n * A `() => string` for `createStorefrontClient({ token })` that reads `file`, caches it and looks again every\n * `ttlMs` (30 s): the file is read only when its mtime or size changed. A failed re-read keeps the last good token\n * (a rotation in progress never takes the store down); only a first read that fails throws.\n */\nexport function createFileTokenSource(file: string, options: FileTokenSourceOptions = {}): () => string {\n const ttl = options.ttlMs ?? 30_000\n const now = options.now ?? Date.now\n let token = ''\n let stamp = ''\n let checkedAt = -Infinity\n return () => {\n if (token && now() - checkedAt < ttl) return token\n checkedAt = now()\n try {\n const st = statSync(file)\n const next = `${st.mtimeMs}:${st.size}`\n if (!token || next !== stamp) {\n const text = readFileSync(file, 'utf8').trim()\n if (text) {\n token = text\n stamp = next\n } else if (!token) {\n throw new Error('token file is empty')\n }\n }\n } catch (err) {\n if (!token) throw new Error(`Cannot read the storefront token file ${file}: ${err instanceof Error ? err.message : String(err)}`)\n }\n return token\n }\n}\n\nexport type TokenEnv = Record<string, string | undefined>\n\n/**\n * The store's token from the environment, in this order:\n * 1. `STOREFRONT_TOKEN_FILE` (path),\n * 2. `$CREDENTIALS_DIRECTORY/storefront_token` (systemd `LoadCredential=`),\n * 3. `STOREFRONT_TOKEN` (the plain variable, as before).\n * Files come back as a rotating source (re-read every 30 s); the variable as a plain string. `undefined` when\n * nothing is configured.\n */\nexport function storefrontTokenFromEnv(env: TokenEnv = process.env, options: FileTokenSourceOptions = {}): string | (() => string) | undefined {\n const file = env.STOREFRONT_TOKEN_FILE?.trim()\n if (file) return createFileTokenSource(file, options)\n const dir = env.CREDENTIALS_DIRECTORY?.trim()\n if (dir) {\n const f = join(dir, 'storefront_token')\n if (existsSync(f)) return createFileTokenSource(f, options)\n }\n const plain = env.STOREFRONT_TOKEN?.trim()\n return plain || undefined\n}\n\n// ---------------------------------------------------------------------------\n// Atomic write\n// ---------------------------------------------------------------------------\n\n/** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */\nexport function writeFileAtomic(file: string, content: string, mode = 0o600): void {\n const dir = dirname(file)\n mkdirSync(dir, { recursive: true })\n const tmp = join(dir, `.${file.split('/').pop()}.${process.pid}.${Date.now().toString(36)}.tmp`)\n const fd = openSync(tmp, 'wx', mode)\n try {\n writeSync(fd, content)\n fsyncSync(fd)\n } catch (err) {\n closeSync(fd)\n try {\n unlinkSync(tmp)\n } catch {\n /* best effort */\n }\n throw err\n }\n closeSync(fd)\n try {\n chmodSync(tmp, mode) // umask can only remove bits at creation; make it exact\n renameSync(tmp, file)\n } catch (err) {\n try {\n unlinkSync(tmp)\n } catch {\n /* best effort */\n }\n throw err\n }\n}\n\n// ---------------------------------------------------------------------------\n// Connection protocol (OPS: docs/STOREFRONT-GATEWAY.md, \"Conexión de tienda\")\n// ---------------------------------------------------------------------------\n\n/** The connection a key belongs to, as OPS describes it. */\nexport type ConnectionBrief = { id: string; name: string; kind: string; profile: string; environment: string }\n\n/** What `redeem` and `rotate` hand over (inside `data`). `rotate` carries no `rotation_days` or `api_base`. */\nexport type ConnectionGrant = {\n token: string\n /** Always `null` today. */\n publishable_token: string | null\n connection: ConnectionBrief\n scopes: string[]\n rotation_days?: number\n /** `{OPS}/api/gw`: the base URL for `createStorefrontClient`. */\n api_base?: string\n /** After a rotation: until when the previous key still works. */\n grace_until?: string | null\n}\n\n/** `POST /heartbeat` answer (inside `data`). */\nexport type HeartbeatResult = {\n status: string\n scopes: string[]\n missing_scopes: string[]\n next_rotation_at?: string | null\n rotate_now?: boolean\n server_time?: string\n}\n\n/** `client` block of `redeem` and `heartbeat` requests. */\nexport type ConnectClientInfo = { name: string; version: string; runtime: string; hostname: string }\n\nexport function connectClientInfo(): ConnectClientInfo {\n return { name: '@asteby/storefront', version: SDK_VERSION, runtime: `node ${process.version}`, hostname: hostname() }\n}\n\n/** Why a connection call failed, in terms an operator can act on. */\nexport type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response'\n\nexport class ConnectError extends Error {\n override readonly name = 'ConnectError'\n constructor(\n readonly kind: ConnectErrorKind,\n message: string,\n readonly status = 0,\n /** `Retry-After` of a 429, in seconds. */\n readonly retryAfterSeconds?: number,\n ) {\n super(message)\n }\n}\n\nconst trimSlash = (u: string) => u.replace(/\\/+$/, '')\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v)\n\n/** The kernel envelope `{success:true,data}` is unwrapped; a flat object is accepted as is. */\nexport function unwrapEnvelope(body: unknown): unknown {\n return isObj(body) && typeof body.success === 'boolean' && 'data' in body ? body.data : body\n}\n\nasync function post<T>(fetchImpl: FetchLike, url: string, body: unknown, bearer?: string): Promise<T> {\n let res: Response\n try {\n res = await fetchImpl(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}) },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new ConnectError('network', 'No pudimos comunicarnos con OPS. Revisa la dirección y tu conexión a internet.')\n }\n const text = await res.text().catch(() => '')\n let json: unknown\n try {\n json = text ? JSON.parse(text) : undefined\n } catch {\n json = undefined\n }\n if (!res.ok) {\n // Errors: `{success:false, error:\"<code>\", message:\"<es-MX>\"}` (or `error:{code,message}`).\n const o = isObj(json) ? json : {}\n const e = isObj(o.error) ? o.error : undefined\n const code = typeof o.error === 'string' ? o.error : typeof e?.code === 'string' ? e.code : ''\n const message = typeof o.message === 'string' ? o.message : typeof e?.message === 'string' ? e.message : ''\n if (res.status === 429) {\n const ra = Number(res.headers.get('Retry-After'))\n throw new ConnectError('rate_limited', message || 'Demasiados intentos. Espera un momento y vuelve a intentarlo.', 429, Number.isFinite(ra) && ra > 0 ? ra : undefined)\n }\n if (res.status === 401 || code === 'invalid_token') throw new ConnectError('unauthorized', message || 'Clave inválida.', res.status)\n if (code === 'invalid_code') throw new ConnectError('invalid_code', message || 'El código no es válido o ya venció. Genera uno nuevo desde OPS.', res.status)\n if (res.status >= 500) throw new ConnectError('server', message || 'OPS no pudo atender la solicitud. Inténtalo de nuevo en un momento.', res.status)\n throw new ConnectError('server', message || `OPS respondió ${res.status}.`, res.status)\n }\n if (json === undefined) throw new ConnectError('invalid_response', 'OPS respondió algo que no entendimos.', res.status)\n return unwrapEnvelope(json) as T\n}\n\nfunction assertGrant(r: unknown): asserts r is ConnectionGrant {\n const o = r as Partial<ConnectionGrant> | undefined\n if (!isObj(o) || typeof o.token !== 'string' || !o.token || !isObj(o.connection) || typeof o.connection.id !== 'string' || !Array.isArray(o.scopes)) {\n throw new ConnectError('invalid_response', 'OPS respondió algo que no entendimos.')\n }\n}\n\nexport type RedeemOptions = { opsUrl: string; code: string; fetch?: FetchLike }\n\n/** Exchanges the one-time connection code for the store's key. `POST {ops}/api/connect/v1/redeem`. */\nexport async function redeemConnectionCode(opts: RedeemOptions): Promise<ConnectionGrant> {\n const fetchImpl = opts.fetch ?? (globalThis.fetch.bind(globalThis) as FetchLike)\n const res = await post<unknown>(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client: connectClientInfo() })\n assertGrant(res)\n return res\n}\n\n/** What is kept next to the token file: everything but the secret. */\nexport type ConnectionState = {\n ops_url: string\n api_base: string\n connection: ConnectionBrief\n scopes: string[]\n rotation_days: number\n /** ISO time from which the key should be rotated (`now + rotation_days`, or the heartbeat's `next_rotation_at`). */\n rotate_after: string\n grace_until?: string | null\n}\n\nexport const stateFileFor = (tokenFile: string) => `${tokenFile}.json`\n\nconst DEFAULT_ROTATION_DAYS = 90\nconst DAY_MS = 86_400_000\n\n/** Builds the state after a redeem/rotate; a rotation keeps the `rotation_days` and `api_base` it does not carry. */\nexport function stateFromGrant(g: ConnectionGrant, opsUrl: string, prev: ConnectionState | null, now = Date.now()): ConnectionState {\n const days = g.rotation_days && g.rotation_days > 0 ? g.rotation_days : (prev?.rotation_days ?? DEFAULT_ROTATION_DAYS)\n return {\n ops_url: trimSlash(opsUrl),\n api_base: g.api_base || prev?.api_base || `${trimSlash(opsUrl)}/api/gw`,\n connection: g.connection,\n scopes: g.scopes,\n rotation_days: days,\n rotate_after: new Date(now + days * DAY_MS).toISOString(),\n ...(g.grace_until ? { grace_until: g.grace_until } : {}),\n }\n}\n\n/** Saves a redeemed/rotated connection: the key (0600, atomic) and its non-secret state beside it. */\nexport function saveConnection(tokenFile: string, g: ConnectionGrant, state: ConnectionState): void {\n // State first: if the process dies in between, the old key still works and rotation is just retried.\n writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + '\\n')\n writeFileAtomic(tokenFile, g.token + '\\n')\n}\n\nexport function loadConnectionState(tokenFile: string): ConnectionState | null {\n try {\n return JSON.parse(readFileSync(stateFileFor(tokenFile), 'utf8')) as ConnectionState\n } catch {\n return null\n }\n}\n\nexport type ConnectStoreOptions = {\n /** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */\n opsUrl?: string\n /** File `connect` wrote (default: `STOREFRONT_TOKEN_FILE`, then `./storefront_token`). */\n tokenFile?: string\n fetch?: FetchLike\n /** Clock, for tests. */\n now?: () => number\n /** Called with a short message when a heartbeat or rotation fails. It never receives a key. */\n onError?: (message: string) => void\n}\n\nexport type KeepFreshOptions = {\n /** Heartbeat period (default 10 min). A `429 Retry-After` longer than this delays the next one. */\n intervalMs?: number\n}\n\nexport type TickResult = 'ok' | 'rotated' | 'failed'\n\n/**\n * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and rotates the key when OPS\n * says `rotate_now`, or `next_rotation_at` / `rotate_after` has passed (`POST /api/connect/v1/rotate`, Bearer = current\n * `sk_`). It writes the new file; a `createFileTokenSource` client picks it up within its re-read interval, and the\n * old key keeps working during OPS's grace period. It never throws and never keeps the process alive.\n */\nexport function connectStore(options: ConnectStoreOptions = {}) {\n const tokenFile = resolve(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? 'storefront_token')\n const fetchImpl = options.fetch ?? (globalThis.fetch.bind(globalThis) as FetchLike)\n const now = options.now ?? Date.now\n let retryAfterMs = 0\n const report = (m: string) => {\n try {\n options.onError?.(m)\n } catch {\n /* ignore */\n }\n }\n\n async function tick(): Promise<TickResult> {\n retryAfterMs = 0\n let state = loadConnectionState(tokenFile)\n const opsUrl = options.opsUrl ?? state?.ops_url\n let token: string\n try {\n token = readFileSync(tokenFile, 'utf8').trim()\n } catch {\n report(`No se pudo leer el archivo de la clave (${tokenFile}).`)\n return 'failed'\n }\n if (!opsUrl || !token) {\n report('Falta la dirección de OPS o la clave: vuelve a ejecutar `npx @asteby/storefront connect`.')\n return 'failed'\n }\n const base = `${trimSlash(opsUrl)}/api/connect/v1`\n try {\n const hb = await post<HeartbeatResult>(fetchImpl, `${base}/heartbeat`, { client: connectClientInfo() }, token)\n if (state && hb?.next_rotation_at && hb.next_rotation_at !== state.rotate_after) {\n state = { ...state, rotate_after: hb.next_rotation_at }\n writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + '\\n')\n }\n const due = Date.parse(hb?.next_rotation_at ?? state?.rotate_after ?? '')\n if (hb?.rotate_now === true || (!Number.isNaN(due) && now() >= due)) {\n const next = await post<unknown>(fetchImpl, `${base}/rotate`, {}, token)\n assertGrant(next)\n saveConnection(tokenFile, next, stateFromGrant(next, opsUrl, state, now()))\n return 'rotated'\n }\n return 'ok'\n } catch (err) {\n if (err instanceof ConnectError && err.retryAfterSeconds) retryAfterMs = err.retryAfterSeconds * 1000\n report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Falló el latido de la conexión (${maskToken(token)}).`)\n return 'failed'\n }\n }\n\n return {\n tokenFile,\n /** One heartbeat (+ rotation when due). Resolves, never rejects. */\n tick,\n /** Starts the periodic heartbeat; returns `stop()`. */\n keepFresh(opts: KeepFreshOptions = {}): () => void {\n const interval = opts.intervalMs ?? 600_000\n let stopped = false\n let timer: ReturnType<typeof setTimeout> | undefined\n const loop = async () => {\n await tick()\n if (stopped) return\n timer = setTimeout(() => void loop(), Math.max(interval, retryAfterMs))\n timer.unref?.()\n }\n void loop()\n return () => {\n stopped = true\n if (timer) clearTimeout(timer)\n }\n },\n }\n}\n","import type { StorefrontClient } from './client.js'\nimport { isStorefrontError } from './errors.js'\nimport type { TokenIntrospection } from './types.js'\n\n/** `sk_…a91f`: the prefix and the last four characters, never enough to use the token. */\nexport function maskToken(token: string | null | undefined): string {\n if (!token) return '(sin token)'\n const prefix = /^[a-z]{2}_/.exec(token)?.[0] ?? ''\n return token.length <= prefix.length + 8 ? `${prefix}…` : `${prefix}…${token.slice(-4)}`\n}\n\nexport type DiagnoseOptions = {\n /** Where the warning goes (default `console.warn`). Receives ONE line per problem. */\n log?: (message: string) => void\n /** Name of the connection, shown in the repair path. */\n connectionName?: string\n /** Give up after this long (default 3000 ms). */\n timeoutMs?: number\n}\n\n/** Where the operator repairs a connection in OPS (es-MX, as the screen names it). */\nexport function repairPath(connectionName?: string): string {\n return `OPS → Conexiones y API → ${connectionName ?? '<conexión>'} → Reparar permisos`\n}\n\n/**\n * Startup check of the store's credential: asks `token.introspect()` and, when scopes are missing, logs ONE clear\n * warning with the repair path (never the token, only `sk_…a91f`). It NEVER throws and is meant to be fired and\n * forgotten, so it cannot delay or break the boot:\n *\n * ```ts\n * void diagnose(storefront)\n * ```\n *\n * Resolves with the introspection, or `null` when it could not be obtained (older OPS, outage, bad token).\n */\nexport async function diagnose(client: StorefrontClient, options: DiagnoseOptions = {}): Promise<TokenIntrospection | null> {\n const log = options.log ?? ((m: string) => console.warn(m))\n const timeoutMs = options.timeoutMs ?? 3000\n const raw = client.config.token\n let mask: string\n try {\n mask = maskToken(typeof raw === 'function' ? raw() : raw)\n } catch {\n mask = maskToken(null)\n }\n try {\n const info = await client.token.introspect({ signal: AbortSignal.timeout(timeoutMs) })\n const missing = Array.isArray(info?.missing_scopes) ? info.missing_scopes : []\n if (missing.length > 0) {\n log(\n `[@asteby/storefront] La credencial ${mask} no tiene permisos que la tienda necesita: ${missing.join(', ')}. ` +\n `Repáralo en ${repairPath(options.connectionName)} (no hace falta cambiar la clave ni reiniciar).`,\n )\n }\n return info ?? null\n } catch (err) {\n if (isStorefrontError(err) && err.status === 401) {\n try {\n log(`[@asteby/storefront] OPS rechazó la credencial ${mask} (401): está revocada o vencida. Vuelve a conectar la tienda en OPS → Conexiones y API.`)\n } catch {\n /* a broken logger must not break boot */\n }\n }\n return null\n }\n}\n"],"mappings":"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","names":[]}
@@ -418,6 +418,10 @@ type InvoiceRequest = {
418
418
  regimen?: string;
419
419
  uso_cfdi?: string;
420
420
  cp?: string;
421
+ /** Alias of `postal_code`, already in production use by some storefronts. */
422
+ zip?: string;
423
+ /** Alias of `use`, already in production use by some storefronts. */
424
+ cfdi_use?: string;
421
425
  /** Alias of `use`. */
422
426
  invoice_use?: string;
423
427
  /** Also save the data to the buyer's account (needs a session). */
@@ -525,7 +529,7 @@ type CustomerCapabilities = {
525
529
  /** `GET {gateway}/_token`: what the token in use is and what it can do. Needs no scope. */
526
530
  type TokenIntrospection = {
527
531
  kind: string;
528
- environment: 'sandbox' | 'production' | (string & {});
532
+ environment: 'sandbox' | 'production' | (string & {}) | null;
529
533
  scopes: string[];
530
534
  /** Scopes the connection should have but the token lacks. Empty when healthy. */
531
535
  missing_scopes: string[];
@@ -418,6 +418,10 @@ type InvoiceRequest = {
418
418
  regimen?: string;
419
419
  uso_cfdi?: string;
420
420
  cp?: string;
421
+ /** Alias of `postal_code`, already in production use by some storefronts. */
422
+ zip?: string;
423
+ /** Alias of `use`, already in production use by some storefronts. */
424
+ cfdi_use?: string;
421
425
  /** Alias of `use`. */
422
426
  invoice_use?: string;
423
427
  /** Also save the data to the buyer's account (needs a session). */
@@ -525,7 +529,7 @@ type CustomerCapabilities = {
525
529
  /** `GET {gateway}/_token`: what the token in use is and what it can do. Needs no scope. */
526
530
  type TokenIntrospection = {
527
531
  kind: string;
528
- environment: 'sandbox' | 'production' | (string & {});
532
+ environment: 'sandbox' | 'production' | (string & {}) | null;
529
533
  scopes: string[];
530
534
  /** Scopes the connection should have but the token lacks. Empty when healthy. */
531
535
  missing_scopes: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/storefront",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Typed client for the Storefront API v1 (catalog, cart, checkout, customer, content) using only global fetch — Node 20+, edge runtimes and browsers — plus an in-memory mock of the whole contract and, in `@asteby/storefront/fiscal-document`, a country-agnostic engine that reads a buyer's fiscal document (PDF) in the browser or Node.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {