@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/dist/mockup.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { aE as Product, aK as ProductVariant } from './types-C-Ley-Mt.cjs';
1
+ import { aE as Product, aK as ProductVariant } from './types-bsdlAumU.cjs';
2
2
  import './geo.cjs';
3
3
 
4
4
  /**
package/dist/mockup.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { aE as Product, aK as ProductVariant } from './types-D7UJxrba.js';
1
+ import { aE as Product, aK as ProductVariant } from './types-B1E3GySa.js';
2
2
  import './geo.js';
3
3
 
4
4
  /**
package/dist/node.cjs CHANGED
@@ -22,13 +22,16 @@ var node_exports = {};
22
22
  __export(node_exports, {
23
23
  ConnectError: () => ConnectError,
24
24
  SDK_VERSION: () => SDK_VERSION,
25
+ connectClientInfo: () => connectClientInfo,
25
26
  connectStore: () => connectStore,
26
27
  createFileTokenSource: () => createFileTokenSource,
27
28
  loadConnectionState: () => loadConnectionState,
28
29
  redeemConnectionCode: () => redeemConnectionCode,
29
30
  saveConnection: () => saveConnection,
30
31
  stateFileFor: () => stateFileFor,
32
+ stateFromGrant: () => stateFromGrant,
31
33
  storefrontTokenFromEnv: () => storefrontTokenFromEnv,
34
+ unwrapEnvelope: () => unwrapEnvelope,
32
35
  writeFileAtomic: () => writeFileAtomic
33
36
  });
34
37
  module.exports = __toCommonJS(node_exports);
@@ -44,7 +47,7 @@ function maskToken(token) {
44
47
  }
45
48
 
46
49
  // src/node.ts
47
- var SDK_VERSION = true ? "0.11.0" : "dev";
50
+ var SDK_VERSION = true ? "0.11.1" : "dev";
48
51
  function createFileTokenSource(file, options = {}) {
49
52
  const ttl = options.ttlMs ?? 3e4;
50
53
  const now = options.now ?? Date.now;
@@ -111,17 +114,26 @@ function writeFileAtomic(file, content, mode = 384) {
111
114
  throw err;
112
115
  }
113
116
  }
117
+ function connectClientInfo() {
118
+ return { name: "@asteby/storefront", version: SDK_VERSION, runtime: `node ${process.version}`, hostname: (0, import_node_os.hostname)() };
119
+ }
114
120
  var ConnectError = class extends Error {
115
- constructor(kind, message, status = 0) {
121
+ constructor(kind, message, status = 0, retryAfterSeconds) {
116
122
  super(message);
117
123
  this.kind = kind;
118
124
  this.status = status;
125
+ this.retryAfterSeconds = retryAfterSeconds;
119
126
  }
120
127
  kind;
121
128
  status;
129
+ retryAfterSeconds;
122
130
  name = "ConnectError";
123
131
  };
124
132
  var trimSlash = (u) => u.replace(/\/+$/, "");
133
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
134
+ function unwrapEnvelope(body) {
135
+ return isObj(body) && typeof body.success === "boolean" && "data" in body ? body.data : body;
136
+ }
125
137
  async function post(fetchImpl, url, body, bearer) {
126
138
  let res;
127
139
  try {
@@ -135,37 +147,59 @@ async function post(fetchImpl, url, body, bearer) {
135
147
  throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
136
148
  }
137
149
  const text = await res.text().catch(() => "");
138
- if (!res.ok) {
139
- if (res.status === 429) throw new ConnectError("rate_limited", "Demasiados intentos. Espera unos minutos e int\xE9ntalo de nuevo.", 429);
140
- if (res.status === 401 || res.status === 403) throw new ConnectError("unauthorized", "OPS rechaz\xF3 la credencial de esta tienda.", res.status);
141
- if (res.status >= 500) throw new ConnectError("server", "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
142
- 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);
143
- }
144
- if (!text) return void 0;
150
+ let json;
145
151
  try {
146
- return JSON.parse(text);
152
+ json = text ? JSON.parse(text) : void 0;
147
153
  } catch {
148
- throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
154
+ json = void 0;
149
155
  }
156
+ if (!res.ok) {
157
+ const o = isObj(json) ? json : {};
158
+ const e = isObj(o.error) ? o.error : void 0;
159
+ const code = typeof o.error === "string" ? o.error : typeof e?.code === "string" ? e.code : "";
160
+ const message = typeof o.message === "string" ? o.message : typeof e?.message === "string" ? e.message : "";
161
+ if (res.status === 429) {
162
+ const ra = Number(res.headers.get("Retry-After"));
163
+ throw new ConnectError("rate_limited", message || "Demasiados intentos. Espera un momento y vuelve a intentarlo.", 429, Number.isFinite(ra) && ra > 0 ? ra : void 0);
164
+ }
165
+ if (res.status === 401 || code === "invalid_token") throw new ConnectError("unauthorized", message || "Clave inv\xE1lida.", res.status);
166
+ 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);
167
+ if (res.status >= 500) throw new ConnectError("server", message || "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
168
+ throw new ConnectError("server", message || `OPS respondi\xF3 ${res.status}.`, res.status);
169
+ }
170
+ if (json === void 0) throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
171
+ return unwrapEnvelope(json);
150
172
  }
151
- function assertRedeem(r) {
173
+ function assertGrant(r) {
152
174
  const o = r;
153
- if (!o || typeof o.secret_token !== "string" || !o.secret_token || typeof o.connection_id !== "string" || !Array.isArray(o.scopes)) {
175
+ if (!isObj(o) || typeof o.token !== "string" || !o.token || !isObj(o.connection) || typeof o.connection.id !== "string" || !Array.isArray(o.scopes)) {
154
176
  throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
155
177
  }
156
178
  }
157
179
  async function redeemConnectionCode(opts) {
158
180
  const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
159
- const client_info = { sdk_version: SDK_VERSION, hostname: (0, import_node_os.hostname)(), ...opts.siteUrl ? { site_url: opts.siteUrl } : {} };
160
- const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client_info });
161
- assertRedeem(res);
181
+ const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client: connectClientInfo() });
182
+ assertGrant(res);
162
183
  return res;
163
184
  }
164
185
  var stateFileFor = (tokenFile) => `${tokenFile}.json`;
165
- function saveConnection(tokenFile, r, opsUrl = r.ops_url) {
166
- 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 };
186
+ var DEFAULT_ROTATION_DAYS = 90;
187
+ var DAY_MS = 864e5;
188
+ function stateFromGrant(g, opsUrl, prev, now = Date.now()) {
189
+ const days = g.rotation_days && g.rotation_days > 0 ? g.rotation_days : prev?.rotation_days ?? DEFAULT_ROTATION_DAYS;
190
+ return {
191
+ ops_url: trimSlash(opsUrl),
192
+ api_base: g.api_base || prev?.api_base || `${trimSlash(opsUrl)}/api/gw`,
193
+ connection: g.connection,
194
+ scopes: g.scopes,
195
+ rotation_days: days,
196
+ rotate_after: new Date(now + days * DAY_MS).toISOString(),
197
+ ...g.grace_until ? { grace_until: g.grace_until } : {}
198
+ };
199
+ }
200
+ function saveConnection(tokenFile, g, state) {
167
201
  writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
168
- writeFileAtomic(tokenFile, r.secret_token + "\n");
202
+ writeFileAtomic(tokenFile, g.token + "\n");
169
203
  }
170
204
  function loadConnectionState(tokenFile) {
171
205
  try {
@@ -178,6 +212,7 @@ function connectStore(options = {}) {
178
212
  const tokenFile = (0, import_node_path.resolve)(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? "storefront_token");
179
213
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
180
214
  const now = options.now ?? Date.now;
215
+ let retryAfterMs = 0;
181
216
  const report = (m) => {
182
217
  try {
183
218
  options.onError?.(m);
@@ -185,31 +220,37 @@ function connectStore(options = {}) {
185
220
  }
186
221
  };
187
222
  async function tick() {
188
- const state = loadConnectionState(tokenFile);
223
+ retryAfterMs = 0;
224
+ let state = loadConnectionState(tokenFile);
189
225
  const opsUrl = options.opsUrl ?? state?.ops_url;
190
226
  let token;
191
227
  try {
192
228
  token = (0, import_node_fs.readFileSync)(tokenFile, "utf8").trim();
193
229
  } catch {
194
- report(`No se pudo leer el archivo del token (${tokenFile}).`);
230
+ report(`No se pudo leer el archivo de la clave (${tokenFile}).`);
195
231
  return "failed";
196
232
  }
197
233
  if (!opsUrl || !token) {
198
- report("Falta la direcci\xF3n de OPS o el token: vuelve a ejecutar `npx @asteby/storefront connect`.");
234
+ report("Falta la direcci\xF3n de OPS o la clave: vuelve a ejecutar `npx @asteby/storefront connect`.");
199
235
  return "failed";
200
236
  }
201
237
  const base = `${trimSlash(opsUrl)}/api/connect/v1`;
202
238
  try {
203
- await post(fetchImpl, `${base}/heartbeat`, { sdk_version: SDK_VERSION, hostname: (0, import_node_os.hostname)() }, token);
204
- const due = state?.rotate_after ? Date.parse(state.rotate_after) : NaN;
205
- if (!Number.isNaN(due) && now() >= due) {
239
+ const hb = await post(fetchImpl, `${base}/heartbeat`, { client: connectClientInfo() }, token);
240
+ if (state && hb?.next_rotation_at && hb.next_rotation_at !== state.rotate_after) {
241
+ state = { ...state, rotate_after: hb.next_rotation_at };
242
+ writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
243
+ }
244
+ const due = Date.parse(hb?.next_rotation_at ?? state?.rotate_after ?? "");
245
+ if (hb?.rotate_now === true || !Number.isNaN(due) && now() >= due) {
206
246
  const next = await post(fetchImpl, `${base}/rotate`, {}, token);
207
- assertRedeem(next);
208
- saveConnection(tokenFile, { ...next, ops_url: next.ops_url || opsUrl }, opsUrl);
247
+ assertGrant(next);
248
+ saveConnection(tokenFile, next, stateFromGrant(next, opsUrl, state, now()));
209
249
  return "rotated";
210
250
  }
211
251
  return "ok";
212
252
  } catch (err) {
253
+ if (err instanceof ConnectError && err.retryAfterSeconds) retryAfterMs = err.retryAfterSeconds * 1e3;
213
254
  report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Fall\xF3 el latido de la conexi\xF3n (${maskToken(token)}).`);
214
255
  return "failed";
215
256
  }
@@ -220,10 +261,20 @@ function connectStore(options = {}) {
220
261
  tick,
221
262
  /** Starts the periodic heartbeat; returns `stop()`. */
222
263
  keepFresh(opts = {}) {
223
- const timer = setInterval(() => void tick(), opts.intervalMs ?? 6e5);
224
- timer.unref?.();
225
- void tick();
226
- return () => clearInterval(timer);
264
+ const interval = opts.intervalMs ?? 6e5;
265
+ let stopped = false;
266
+ let timer;
267
+ const loop = async () => {
268
+ await tick();
269
+ if (stopped) return;
270
+ timer = setTimeout(() => void loop(), Math.max(interval, retryAfterMs));
271
+ timer.unref?.();
272
+ };
273
+ void loop();
274
+ return () => {
275
+ stopped = true;
276
+ if (timer) clearTimeout(timer);
277
+ };
227
278
  }
228
279
  };
229
280
  }
@@ -231,13 +282,16 @@ function connectStore(options = {}) {
231
282
  0 && (module.exports = {
232
283
  ConnectError,
233
284
  SDK_VERSION,
285
+ connectClientInfo,
234
286
  connectStore,
235
287
  createFileTokenSource,
236
288
  loadConnectionState,
237
289
  redeemConnectionCode,
238
290
  saveConnection,
239
291
  stateFileFor,
292
+ stateFromGrant,
240
293
  storefrontTokenFromEnv,
294
+ unwrapEnvelope,
241
295
  writeFileAtomic
242
296
  });
243
297
  //# sourceMappingURL=node.cjs.map
package/dist/node.cjs.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":[]}
package/dist/node.d.cts CHANGED
@@ -25,48 +25,81 @@ type TokenEnv = Record<string, string | undefined>;
25
25
  declare function storefrontTokenFromEnv(env?: TokenEnv, options?: FileTokenSourceOptions): string | (() => string) | undefined;
26
26
  /** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */
27
27
  declare function writeFileAtomic(file: string, content: string, mode?: number): void;
28
- type RedeemResponse = {
29
- ops_url: string;
30
- connection_id: string;
28
+ /** The connection a key belongs to, as OPS describes it. */
29
+ type ConnectionBrief = {
30
+ id: string;
31
+ name: string;
32
+ kind: string;
33
+ profile: string;
31
34
  environment: string;
32
- secret_token: string;
33
- publishable_token?: string;
35
+ };
36
+ /** What `redeem` and `rotate` hand over (inside `data`). `rotate` carries no `rotation_days` or `api_base`. */
37
+ type ConnectionGrant = {
38
+ token: string;
39
+ /** Always `null` today. */
40
+ publishable_token: string | null;
41
+ connection: ConnectionBrief;
34
42
  scopes: string[];
35
- /** ISO time from which the token should be rotated. */
36
- rotate_after: string;
43
+ rotation_days?: number;
44
+ /** `{OPS}/api/gw`: the base URL for `createStorefrontClient`. */
45
+ api_base?: string;
46
+ /** After a rotation: until when the previous key still works. */
47
+ grace_until?: string | null;
37
48
  };
49
+ /** `POST /heartbeat` answer (inside `data`). */
50
+ type HeartbeatResult = {
51
+ status: string;
52
+ scopes: string[];
53
+ missing_scopes: string[];
54
+ next_rotation_at?: string | null;
55
+ rotate_now?: boolean;
56
+ server_time?: string;
57
+ };
58
+ /** `client` block of `redeem` and `heartbeat` requests. */
38
59
  type ConnectClientInfo = {
39
- sdk_version: string;
60
+ name: string;
61
+ version: string;
62
+ runtime: string;
40
63
  hostname: string;
41
- site_url?: string;
42
64
  };
65
+ declare function connectClientInfo(): ConnectClientInfo;
43
66
  /** Why a connection call failed, in terms an operator can act on. */
44
67
  type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response';
45
68
  declare class ConnectError extends Error {
46
69
  readonly kind: ConnectErrorKind;
47
70
  readonly status: number;
71
+ /** `Retry-After` of a 429, in seconds. */
72
+ readonly retryAfterSeconds?: number | undefined;
48
73
  readonly name = "ConnectError";
49
- constructor(kind: ConnectErrorKind, message: string, status?: number);
74
+ constructor(kind: ConnectErrorKind, message: string, status?: number,
75
+ /** `Retry-After` of a 429, in seconds. */
76
+ retryAfterSeconds?: number | undefined);
50
77
  }
78
+ /** The kernel envelope `{success:true,data}` is unwrapped; a flat object is accepted as is. */
79
+ declare function unwrapEnvelope(body: unknown): unknown;
51
80
  type RedeemOptions = {
52
81
  opsUrl: string;
53
82
  code: string;
54
- siteUrl?: string;
55
83
  fetch?: FetchLike;
56
84
  };
57
- /** Exchanges the one-time connection code for the store's token. `POST {ops}/api/connect/v1/redeem`. */
58
- declare function redeemConnectionCode(opts: RedeemOptions): Promise<RedeemResponse>;
85
+ /** Exchanges the one-time connection code for the store's key. `POST {ops}/api/connect/v1/redeem`. */
86
+ declare function redeemConnectionCode(opts: RedeemOptions): Promise<ConnectionGrant>;
59
87
  /** What is kept next to the token file: everything but the secret. */
60
88
  type ConnectionState = {
61
89
  ops_url: string;
62
- connection_id: string;
63
- environment: string;
90
+ api_base: string;
91
+ connection: ConnectionBrief;
64
92
  scopes: string[];
93
+ rotation_days: number;
94
+ /** ISO time from which the key should be rotated (`now + rotation_days`, or the heartbeat's `next_rotation_at`). */
65
95
  rotate_after: string;
96
+ grace_until?: string | null;
66
97
  };
67
98
  declare const stateFileFor: (tokenFile: string) => string;
68
- /** Saves a redeemed/rotated connection: the token (0600, atomic) and its non-secret state beside it. */
69
- declare function saveConnection(tokenFile: string, r: RedeemResponse, opsUrl?: string): void;
99
+ /** Builds the state after a redeem/rotate; a rotation keeps the `rotation_days` and `api_base` it does not carry. */
100
+ declare function stateFromGrant(g: ConnectionGrant, opsUrl: string, prev: ConnectionState | null, now?: number): ConnectionState;
101
+ /** Saves a redeemed/rotated connection: the key (0600, atomic) and its non-secret state beside it. */
102
+ declare function saveConnection(tokenFile: string, g: ConnectionGrant, state: ConnectionState): void;
70
103
  declare function loadConnectionState(tokenFile: string): ConnectionState | null;
71
104
  type ConnectStoreOptions = {
72
105
  /** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */
@@ -76,25 +109,26 @@ type ConnectStoreOptions = {
76
109
  fetch?: FetchLike;
77
110
  /** Clock, for tests. */
78
111
  now?: () => number;
79
- /** Called with a short message when a heartbeat or rotation fails. It never receives a token. */
112
+ /** Called with a short message when a heartbeat or rotation fails. It never receives a key. */
80
113
  onError?: (message: string) => void;
81
114
  };
82
115
  type KeepFreshOptions = {
83
- /** Heartbeat period (default 10 min). */
116
+ /** Heartbeat period (default 10 min). A `429 Retry-After` longer than this delays the next one. */
84
117
  intervalMs?: number;
85
118
  };
119
+ type TickResult = 'ok' | 'rotated' | 'failed';
86
120
  /**
87
- * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and, when `rotate_after`
88
- * passes, rotates the token (`POST /api/connect/v1/rotate`, Bearer = current `sk_`), writes the new file and lets a
89
- * `createFileTokenSource` client pick it up within its re-read interval. The old token keeps working during a grace
90
- * period set by OPS. It never throws and never keeps the process alive.
121
+ * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and rotates the key when OPS
122
+ * says `rotate_now`, or `next_rotation_at` / `rotate_after` has passed (`POST /api/connect/v1/rotate`, Bearer = current
123
+ * `sk_`). It writes the new file; a `createFileTokenSource` client picks it up within its re-read interval, and the
124
+ * old key keeps working during OPS's grace period. It never throws and never keeps the process alive.
91
125
  */
92
126
  declare function connectStore(options?: ConnectStoreOptions): {
93
127
  tokenFile: string;
94
128
  /** One heartbeat (+ rotation when due). Resolves, never rejects. */
95
- tick: () => Promise<"ok" | "rotated" | "failed">;
129
+ tick: () => Promise<TickResult>;
96
130
  /** Starts the periodic heartbeat; returns `stop()`. */
97
131
  keepFresh(opts?: KeepFreshOptions): () => void;
98
132
  };
99
133
 
100
- export { type ConnectClientInfo, ConnectError, type ConnectErrorKind, type ConnectStoreOptions, type ConnectionState, type FileTokenSourceOptions, type KeepFreshOptions, type RedeemOptions, type RedeemResponse, SDK_VERSION, type TokenEnv, connectStore, createFileTokenSource, loadConnectionState, redeemConnectionCode, saveConnection, stateFileFor, storefrontTokenFromEnv, writeFileAtomic };
134
+ export { type ConnectClientInfo, ConnectError, type ConnectErrorKind, type ConnectStoreOptions, type ConnectionBrief, type ConnectionGrant, type ConnectionState, type FileTokenSourceOptions, type HeartbeatResult, type KeepFreshOptions, type RedeemOptions, SDK_VERSION, type TickResult, type TokenEnv, connectClientInfo, connectStore, createFileTokenSource, loadConnectionState, redeemConnectionCode, saveConnection, stateFileFor, stateFromGrant, storefrontTokenFromEnv, unwrapEnvelope, writeFileAtomic };
package/dist/node.d.ts CHANGED
@@ -25,48 +25,81 @@ type TokenEnv = Record<string, string | undefined>;
25
25
  declare function storefrontTokenFromEnv(env?: TokenEnv, options?: FileTokenSourceOptions): string | (() => string) | undefined;
26
26
  /** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */
27
27
  declare function writeFileAtomic(file: string, content: string, mode?: number): void;
28
- type RedeemResponse = {
29
- ops_url: string;
30
- connection_id: string;
28
+ /** The connection a key belongs to, as OPS describes it. */
29
+ type ConnectionBrief = {
30
+ id: string;
31
+ name: string;
32
+ kind: string;
33
+ profile: string;
31
34
  environment: string;
32
- secret_token: string;
33
- publishable_token?: string;
35
+ };
36
+ /** What `redeem` and `rotate` hand over (inside `data`). `rotate` carries no `rotation_days` or `api_base`. */
37
+ type ConnectionGrant = {
38
+ token: string;
39
+ /** Always `null` today. */
40
+ publishable_token: string | null;
41
+ connection: ConnectionBrief;
34
42
  scopes: string[];
35
- /** ISO time from which the token should be rotated. */
36
- rotate_after: string;
43
+ rotation_days?: number;
44
+ /** `{OPS}/api/gw`: the base URL for `createStorefrontClient`. */
45
+ api_base?: string;
46
+ /** After a rotation: until when the previous key still works. */
47
+ grace_until?: string | null;
37
48
  };
49
+ /** `POST /heartbeat` answer (inside `data`). */
50
+ type HeartbeatResult = {
51
+ status: string;
52
+ scopes: string[];
53
+ missing_scopes: string[];
54
+ next_rotation_at?: string | null;
55
+ rotate_now?: boolean;
56
+ server_time?: string;
57
+ };
58
+ /** `client` block of `redeem` and `heartbeat` requests. */
38
59
  type ConnectClientInfo = {
39
- sdk_version: string;
60
+ name: string;
61
+ version: string;
62
+ runtime: string;
40
63
  hostname: string;
41
- site_url?: string;
42
64
  };
65
+ declare function connectClientInfo(): ConnectClientInfo;
43
66
  /** Why a connection call failed, in terms an operator can act on. */
44
67
  type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response';
45
68
  declare class ConnectError extends Error {
46
69
  readonly kind: ConnectErrorKind;
47
70
  readonly status: number;
71
+ /** `Retry-After` of a 429, in seconds. */
72
+ readonly retryAfterSeconds?: number | undefined;
48
73
  readonly name = "ConnectError";
49
- constructor(kind: ConnectErrorKind, message: string, status?: number);
74
+ constructor(kind: ConnectErrorKind, message: string, status?: number,
75
+ /** `Retry-After` of a 429, in seconds. */
76
+ retryAfterSeconds?: number | undefined);
50
77
  }
78
+ /** The kernel envelope `{success:true,data}` is unwrapped; a flat object is accepted as is. */
79
+ declare function unwrapEnvelope(body: unknown): unknown;
51
80
  type RedeemOptions = {
52
81
  opsUrl: string;
53
82
  code: string;
54
- siteUrl?: string;
55
83
  fetch?: FetchLike;
56
84
  };
57
- /** Exchanges the one-time connection code for the store's token. `POST {ops}/api/connect/v1/redeem`. */
58
- declare function redeemConnectionCode(opts: RedeemOptions): Promise<RedeemResponse>;
85
+ /** Exchanges the one-time connection code for the store's key. `POST {ops}/api/connect/v1/redeem`. */
86
+ declare function redeemConnectionCode(opts: RedeemOptions): Promise<ConnectionGrant>;
59
87
  /** What is kept next to the token file: everything but the secret. */
60
88
  type ConnectionState = {
61
89
  ops_url: string;
62
- connection_id: string;
63
- environment: string;
90
+ api_base: string;
91
+ connection: ConnectionBrief;
64
92
  scopes: string[];
93
+ rotation_days: number;
94
+ /** ISO time from which the key should be rotated (`now + rotation_days`, or the heartbeat's `next_rotation_at`). */
65
95
  rotate_after: string;
96
+ grace_until?: string | null;
66
97
  };
67
98
  declare const stateFileFor: (tokenFile: string) => string;
68
- /** Saves a redeemed/rotated connection: the token (0600, atomic) and its non-secret state beside it. */
69
- declare function saveConnection(tokenFile: string, r: RedeemResponse, opsUrl?: string): void;
99
+ /** Builds the state after a redeem/rotate; a rotation keeps the `rotation_days` and `api_base` it does not carry. */
100
+ declare function stateFromGrant(g: ConnectionGrant, opsUrl: string, prev: ConnectionState | null, now?: number): ConnectionState;
101
+ /** Saves a redeemed/rotated connection: the key (0600, atomic) and its non-secret state beside it. */
102
+ declare function saveConnection(tokenFile: string, g: ConnectionGrant, state: ConnectionState): void;
70
103
  declare function loadConnectionState(tokenFile: string): ConnectionState | null;
71
104
  type ConnectStoreOptions = {
72
105
  /** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */
@@ -76,25 +109,26 @@ type ConnectStoreOptions = {
76
109
  fetch?: FetchLike;
77
110
  /** Clock, for tests. */
78
111
  now?: () => number;
79
- /** Called with a short message when a heartbeat or rotation fails. It never receives a token. */
112
+ /** Called with a short message when a heartbeat or rotation fails. It never receives a key. */
80
113
  onError?: (message: string) => void;
81
114
  };
82
115
  type KeepFreshOptions = {
83
- /** Heartbeat period (default 10 min). */
116
+ /** Heartbeat period (default 10 min). A `429 Retry-After` longer than this delays the next one. */
84
117
  intervalMs?: number;
85
118
  };
119
+ type TickResult = 'ok' | 'rotated' | 'failed';
86
120
  /**
87
- * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and, when `rotate_after`
88
- * passes, rotates the token (`POST /api/connect/v1/rotate`, Bearer = current `sk_`), writes the new file and lets a
89
- * `createFileTokenSource` client pick it up within its re-read interval. The old token keeps working during a grace
90
- * period set by OPS. It never throws and never keeps the process alive.
121
+ * The store's side of the connection after `connect`: `keepFresh()` sends a heartbeat and rotates the key when OPS
122
+ * says `rotate_now`, or `next_rotation_at` / `rotate_after` has passed (`POST /api/connect/v1/rotate`, Bearer = current
123
+ * `sk_`). It writes the new file; a `createFileTokenSource` client picks it up within its re-read interval, and the
124
+ * old key keeps working during OPS's grace period. It never throws and never keeps the process alive.
91
125
  */
92
126
  declare function connectStore(options?: ConnectStoreOptions): {
93
127
  tokenFile: string;
94
128
  /** One heartbeat (+ rotation when due). Resolves, never rejects. */
95
- tick: () => Promise<"ok" | "rotated" | "failed">;
129
+ tick: () => Promise<TickResult>;
96
130
  /** Starts the periodic heartbeat; returns `stop()`. */
97
131
  keepFresh(opts?: KeepFreshOptions): () => void;
98
132
  };
99
133
 
100
- export { type ConnectClientInfo, ConnectError, type ConnectErrorKind, type ConnectStoreOptions, type ConnectionState, type FileTokenSourceOptions, type KeepFreshOptions, type RedeemOptions, type RedeemResponse, SDK_VERSION, type TokenEnv, connectStore, createFileTokenSource, loadConnectionState, redeemConnectionCode, saveConnection, stateFileFor, storefrontTokenFromEnv, writeFileAtomic };
134
+ export { type ConnectClientInfo, ConnectError, type ConnectErrorKind, type ConnectStoreOptions, type ConnectionBrief, type ConnectionGrant, type ConnectionState, type FileTokenSourceOptions, type HeartbeatResult, type KeepFreshOptions, type RedeemOptions, SDK_VERSION, type TickResult, type TokenEnv, connectClientInfo, connectStore, createFileTokenSource, loadConnectionState, redeemConnectionCode, saveConnection, stateFileFor, stateFromGrant, storefrontTokenFromEnv, unwrapEnvelope, writeFileAtomic };