@asteby/storefront 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cli.js +65 -30
- package/dist/fiscal-document.d.cts +2 -2
- package/dist/fiscal-document.d.ts +2 -2
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +3 -3
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +1 -1
- package/dist/mock.d.ts +1 -1
- package/dist/mock.js +3 -3
- package/dist/mock.js.map +1 -1
- package/dist/mockup.d.cts +1 -1
- package/dist/mockup.d.ts +1 -1
- package/dist/node.cjs +85 -31
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +59 -25
- package/dist/node.d.ts +59 -25
- package/dist/node.js +82 -31
- package/dist/node.js.map +1 -1
- package/dist/{types-D7UJxrba.d.ts → types-B1E3GySa.d.ts} +1 -1
- package/dist/{types-C-Ley-Mt.d.cts → types-bsdlAumU.d.cts} +1 -1
- package/package.json +1 -1
package/dist/mockup.d.cts
CHANGED
package/dist/mockup.d.ts
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
json = text ? JSON.parse(text) : void 0;
|
|
147
153
|
} catch {
|
|
148
|
-
|
|
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
|
|
173
|
+
function assertGrant(r) {
|
|
152
174
|
const o = r;
|
|
153
|
-
if (!o || typeof o.
|
|
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
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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`, {
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
saveConnection(tokenFile,
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
declare function redeemConnectionCode(opts: RedeemOptions): Promise<
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
/**
|
|
69
|
-
declare function
|
|
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
|
|
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
|
|
88
|
-
*
|
|
89
|
-
* `createFileTokenSource` client
|
|
90
|
-
*
|
|
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<
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
declare function redeemConnectionCode(opts: RedeemOptions): Promise<
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
/**
|
|
69
|
-
declare function
|
|
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
|
|
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
|
|
88
|
-
*
|
|
89
|
-
* `createFileTokenSource` client
|
|
90
|
-
*
|
|
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<
|
|
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
|
|
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 };
|