@asteby/storefront 0.10.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 +151 -4
- package/dist/cli.js +700 -0
- package/dist/fiscal-document.cjs +659 -0
- package/dist/fiscal-document.cjs.map +1 -0
- package/dist/fiscal-document.d.cts +215 -0
- package/dist/fiscal-document.d.ts +215 -0
- package/dist/fiscal-document.js +641 -0
- package/dist/fiscal-document.js.map +1 -0
- package/dist/index.cjs +97 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -5
- package/dist/index.d.ts +61 -5
- package/dist/index.js +95 -6
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +1072 -11
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +33 -2
- package/dist/mock.d.ts +33 -2
- package/dist/mock.js +1069 -12
- 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 +297 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +134 -0
- package/dist/node.d.ts +134 -0
- package/dist/node.js +260 -0
- package/dist/node.js.map +1 -0
- package/dist/{types-COSwo4ND.d.ts → types-B1E3GySa.d.ts} +293 -3
- package/dist/{types-DdLzlxw4.d.cts → types-bsdlAumU.d.cts} +293 -3
- package/package.json +41 -3
package/dist/node.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/node.ts
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync } from "fs";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { hostname } from "os";
|
|
5
|
+
|
|
6
|
+
// src/diagnose.ts
|
|
7
|
+
function maskToken(token) {
|
|
8
|
+
if (!token) return "(sin token)";
|
|
9
|
+
const prefix = /^[a-z]{2}_/.exec(token)?.[0] ?? "";
|
|
10
|
+
return token.length <= prefix.length + 8 ? `${prefix}\u2026` : `${prefix}\u2026${token.slice(-4)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/node.ts
|
|
14
|
+
var SDK_VERSION = true ? "0.11.1" : "dev";
|
|
15
|
+
function createFileTokenSource(file, options = {}) {
|
|
16
|
+
const ttl = options.ttlMs ?? 3e4;
|
|
17
|
+
const now = options.now ?? Date.now;
|
|
18
|
+
let token = "";
|
|
19
|
+
let stamp = "";
|
|
20
|
+
let checkedAt = -Infinity;
|
|
21
|
+
return () => {
|
|
22
|
+
if (token && now() - checkedAt < ttl) return token;
|
|
23
|
+
checkedAt = now();
|
|
24
|
+
try {
|
|
25
|
+
const st = statSync(file);
|
|
26
|
+
const next = `${st.mtimeMs}:${st.size}`;
|
|
27
|
+
if (!token || next !== stamp) {
|
|
28
|
+
const text = readFileSync(file, "utf8").trim();
|
|
29
|
+
if (text) {
|
|
30
|
+
token = text;
|
|
31
|
+
stamp = next;
|
|
32
|
+
} else if (!token) {
|
|
33
|
+
throw new Error("token file is empty");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (!token) throw new Error(`Cannot read the storefront token file ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
return token;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function storefrontTokenFromEnv(env = process.env, options = {}) {
|
|
43
|
+
const file = env.STOREFRONT_TOKEN_FILE?.trim();
|
|
44
|
+
if (file) return createFileTokenSource(file, options);
|
|
45
|
+
const dir = env.CREDENTIALS_DIRECTORY?.trim();
|
|
46
|
+
if (dir) {
|
|
47
|
+
const f = join(dir, "storefront_token");
|
|
48
|
+
if (existsSync(f)) return createFileTokenSource(f, options);
|
|
49
|
+
}
|
|
50
|
+
const plain = env.STOREFRONT_TOKEN?.trim();
|
|
51
|
+
return plain || void 0;
|
|
52
|
+
}
|
|
53
|
+
function writeFileAtomic(file, content, mode = 384) {
|
|
54
|
+
const dir = dirname(file);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const tmp = join(dir, `.${file.split("/").pop()}.${process.pid}.${Date.now().toString(36)}.tmp`);
|
|
57
|
+
const fd = openSync(tmp, "wx", mode);
|
|
58
|
+
try {
|
|
59
|
+
writeSync(fd, content);
|
|
60
|
+
fsyncSync(fd);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
closeSync(fd);
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(tmp);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
try {
|
|
71
|
+
chmodSync(tmp, mode);
|
|
72
|
+
renameSync(tmp, file);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(tmp);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function connectClientInfo() {
|
|
82
|
+
return { name: "@asteby/storefront", version: SDK_VERSION, runtime: `node ${process.version}`, hostname: hostname() };
|
|
83
|
+
}
|
|
84
|
+
var ConnectError = class extends Error {
|
|
85
|
+
constructor(kind, message, status = 0, retryAfterSeconds) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.kind = kind;
|
|
88
|
+
this.status = status;
|
|
89
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
90
|
+
}
|
|
91
|
+
kind;
|
|
92
|
+
status;
|
|
93
|
+
retryAfterSeconds;
|
|
94
|
+
name = "ConnectError";
|
|
95
|
+
};
|
|
96
|
+
var trimSlash = (u) => u.replace(/\/+$/, "");
|
|
97
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
98
|
+
function unwrapEnvelope(body) {
|
|
99
|
+
return isObj(body) && typeof body.success === "boolean" && "data" in body ? body.data : body;
|
|
100
|
+
}
|
|
101
|
+
async function post(fetchImpl, url, body, bearer) {
|
|
102
|
+
let res;
|
|
103
|
+
try {
|
|
104
|
+
res = await fetchImpl(url, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...bearer ? { Authorization: `Bearer ${bearer}` } : {} },
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
signal: AbortSignal.timeout(15e3)
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
|
|
112
|
+
}
|
|
113
|
+
const text = await res.text().catch(() => "");
|
|
114
|
+
let json;
|
|
115
|
+
try {
|
|
116
|
+
json = text ? JSON.parse(text) : void 0;
|
|
117
|
+
} catch {
|
|
118
|
+
json = void 0;
|
|
119
|
+
}
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const o = isObj(json) ? json : {};
|
|
122
|
+
const e = isObj(o.error) ? o.error : void 0;
|
|
123
|
+
const code = typeof o.error === "string" ? o.error : typeof e?.code === "string" ? e.code : "";
|
|
124
|
+
const message = typeof o.message === "string" ? o.message : typeof e?.message === "string" ? e.message : "";
|
|
125
|
+
if (res.status === 429) {
|
|
126
|
+
const ra = Number(res.headers.get("Retry-After"));
|
|
127
|
+
throw new ConnectError("rate_limited", message || "Demasiados intentos. Espera un momento y vuelve a intentarlo.", 429, Number.isFinite(ra) && ra > 0 ? ra : void 0);
|
|
128
|
+
}
|
|
129
|
+
if (res.status === 401 || code === "invalid_token") throw new ConnectError("unauthorized", message || "Clave inv\xE1lida.", res.status);
|
|
130
|
+
if (code === "invalid_code") throw new ConnectError("invalid_code", message || "El c\xF3digo no es v\xE1lido o ya venci\xF3. Genera uno nuevo desde OPS.", res.status);
|
|
131
|
+
if (res.status >= 500) throw new ConnectError("server", message || "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
|
|
132
|
+
throw new ConnectError("server", message || `OPS respondi\xF3 ${res.status}.`, res.status);
|
|
133
|
+
}
|
|
134
|
+
if (json === void 0) throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
|
|
135
|
+
return unwrapEnvelope(json);
|
|
136
|
+
}
|
|
137
|
+
function assertGrant(r) {
|
|
138
|
+
const o = r;
|
|
139
|
+
if (!isObj(o) || typeof o.token !== "string" || !o.token || !isObj(o.connection) || typeof o.connection.id !== "string" || !Array.isArray(o.scopes)) {
|
|
140
|
+
throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function redeemConnectionCode(opts) {
|
|
144
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
145
|
+
const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client: connectClientInfo() });
|
|
146
|
+
assertGrant(res);
|
|
147
|
+
return res;
|
|
148
|
+
}
|
|
149
|
+
var stateFileFor = (tokenFile) => `${tokenFile}.json`;
|
|
150
|
+
var DEFAULT_ROTATION_DAYS = 90;
|
|
151
|
+
var DAY_MS = 864e5;
|
|
152
|
+
function stateFromGrant(g, opsUrl, prev, now = Date.now()) {
|
|
153
|
+
const days = g.rotation_days && g.rotation_days > 0 ? g.rotation_days : prev?.rotation_days ?? DEFAULT_ROTATION_DAYS;
|
|
154
|
+
return {
|
|
155
|
+
ops_url: trimSlash(opsUrl),
|
|
156
|
+
api_base: g.api_base || prev?.api_base || `${trimSlash(opsUrl)}/api/gw`,
|
|
157
|
+
connection: g.connection,
|
|
158
|
+
scopes: g.scopes,
|
|
159
|
+
rotation_days: days,
|
|
160
|
+
rotate_after: new Date(now + days * DAY_MS).toISOString(),
|
|
161
|
+
...g.grace_until ? { grace_until: g.grace_until } : {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function saveConnection(tokenFile, g, state) {
|
|
165
|
+
writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
|
|
166
|
+
writeFileAtomic(tokenFile, g.token + "\n");
|
|
167
|
+
}
|
|
168
|
+
function loadConnectionState(tokenFile) {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(readFileSync(stateFileFor(tokenFile), "utf8"));
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function connectStore(options = {}) {
|
|
176
|
+
const tokenFile = resolve(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? "storefront_token");
|
|
177
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
178
|
+
const now = options.now ?? Date.now;
|
|
179
|
+
let retryAfterMs = 0;
|
|
180
|
+
const report = (m) => {
|
|
181
|
+
try {
|
|
182
|
+
options.onError?.(m);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
async function tick() {
|
|
187
|
+
retryAfterMs = 0;
|
|
188
|
+
let state = loadConnectionState(tokenFile);
|
|
189
|
+
const opsUrl = options.opsUrl ?? state?.ops_url;
|
|
190
|
+
let token;
|
|
191
|
+
try {
|
|
192
|
+
token = readFileSync(tokenFile, "utf8").trim();
|
|
193
|
+
} catch {
|
|
194
|
+
report(`No se pudo leer el archivo de la clave (${tokenFile}).`);
|
|
195
|
+
return "failed";
|
|
196
|
+
}
|
|
197
|
+
if (!opsUrl || !token) {
|
|
198
|
+
report("Falta la direcci\xF3n de OPS o la clave: vuelve a ejecutar `npx @asteby/storefront connect`.");
|
|
199
|
+
return "failed";
|
|
200
|
+
}
|
|
201
|
+
const base = `${trimSlash(opsUrl)}/api/connect/v1`;
|
|
202
|
+
try {
|
|
203
|
+
const hb = await post(fetchImpl, `${base}/heartbeat`, { client: connectClientInfo() }, token);
|
|
204
|
+
if (state && hb?.next_rotation_at && hb.next_rotation_at !== state.rotate_after) {
|
|
205
|
+
state = { ...state, rotate_after: hb.next_rotation_at };
|
|
206
|
+
writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
|
|
207
|
+
}
|
|
208
|
+
const due = Date.parse(hb?.next_rotation_at ?? state?.rotate_after ?? "");
|
|
209
|
+
if (hb?.rotate_now === true || !Number.isNaN(due) && now() >= due) {
|
|
210
|
+
const next = await post(fetchImpl, `${base}/rotate`, {}, token);
|
|
211
|
+
assertGrant(next);
|
|
212
|
+
saveConnection(tokenFile, next, stateFromGrant(next, opsUrl, state, now()));
|
|
213
|
+
return "rotated";
|
|
214
|
+
}
|
|
215
|
+
return "ok";
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err instanceof ConnectError && err.retryAfterSeconds) retryAfterMs = err.retryAfterSeconds * 1e3;
|
|
218
|
+
report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Fall\xF3 el latido de la conexi\xF3n (${maskToken(token)}).`);
|
|
219
|
+
return "failed";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
tokenFile,
|
|
224
|
+
/** One heartbeat (+ rotation when due). Resolves, never rejects. */
|
|
225
|
+
tick,
|
|
226
|
+
/** Starts the periodic heartbeat; returns `stop()`. */
|
|
227
|
+
keepFresh(opts = {}) {
|
|
228
|
+
const interval = opts.intervalMs ?? 6e5;
|
|
229
|
+
let stopped = false;
|
|
230
|
+
let timer;
|
|
231
|
+
const loop = async () => {
|
|
232
|
+
await tick();
|
|
233
|
+
if (stopped) return;
|
|
234
|
+
timer = setTimeout(() => void loop(), Math.max(interval, retryAfterMs));
|
|
235
|
+
timer.unref?.();
|
|
236
|
+
};
|
|
237
|
+
void loop();
|
|
238
|
+
return () => {
|
|
239
|
+
stopped = true;
|
|
240
|
+
if (timer) clearTimeout(timer);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export {
|
|
246
|
+
ConnectError,
|
|
247
|
+
SDK_VERSION,
|
|
248
|
+
connectClientInfo,
|
|
249
|
+
connectStore,
|
|
250
|
+
createFileTokenSource,
|
|
251
|
+
loadConnectionState,
|
|
252
|
+
redeemConnectionCode,
|
|
253
|
+
saveConnection,
|
|
254
|
+
stateFileFor,
|
|
255
|
+
stateFromGrant,
|
|
256
|
+
storefrontTokenFromEnv,
|
|
257
|
+
unwrapEnvelope,
|
|
258
|
+
writeFileAtomic
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +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 (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":[]}
|