@asteby/storefront 0.9.0 → 0.11.0
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 +181 -4
- package/dist/cli.js +665 -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 +111 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -5
- package/dist/index.d.ts +76 -5
- package/dist/index.js +109 -6
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +1185 -11
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +51 -2
- package/dist/mock.d.ts +51 -2
- package/dist/mock.js +1182 -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 +243 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +100 -0
- package/dist/node.d.ts +100 -0
- package/dist/node.js +209 -0
- package/dist/node.js.map +1 -0
- package/dist/{types-CSMuPSQA.d.cts → types-C-Ley-Mt.d.cts} +333 -3
- package/dist/{types-iGGZ9EiC.d.ts → types-D7UJxrba.d.ts} +333 -3
- package/package.json +41 -3
package/dist/mockup.d.cts
CHANGED
package/dist/mockup.d.ts
CHANGED
package/dist/node.cjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/node.ts
|
|
21
|
+
var node_exports = {};
|
|
22
|
+
__export(node_exports, {
|
|
23
|
+
ConnectError: () => ConnectError,
|
|
24
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
25
|
+
connectStore: () => connectStore,
|
|
26
|
+
createFileTokenSource: () => createFileTokenSource,
|
|
27
|
+
loadConnectionState: () => loadConnectionState,
|
|
28
|
+
redeemConnectionCode: () => redeemConnectionCode,
|
|
29
|
+
saveConnection: () => saveConnection,
|
|
30
|
+
stateFileFor: () => stateFileFor,
|
|
31
|
+
storefrontTokenFromEnv: () => storefrontTokenFromEnv,
|
|
32
|
+
writeFileAtomic: () => writeFileAtomic
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(node_exports);
|
|
35
|
+
var import_node_fs = require("fs");
|
|
36
|
+
var import_node_path = require("path");
|
|
37
|
+
var import_node_os = require("os");
|
|
38
|
+
|
|
39
|
+
// src/diagnose.ts
|
|
40
|
+
function maskToken(token) {
|
|
41
|
+
if (!token) return "(sin token)";
|
|
42
|
+
const prefix = /^[a-z]{2}_/.exec(token)?.[0] ?? "";
|
|
43
|
+
return token.length <= prefix.length + 8 ? `${prefix}\u2026` : `${prefix}\u2026${token.slice(-4)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/node.ts
|
|
47
|
+
var SDK_VERSION = true ? "0.11.0" : "dev";
|
|
48
|
+
function createFileTokenSource(file, options = {}) {
|
|
49
|
+
const ttl = options.ttlMs ?? 3e4;
|
|
50
|
+
const now = options.now ?? Date.now;
|
|
51
|
+
let token = "";
|
|
52
|
+
let stamp = "";
|
|
53
|
+
let checkedAt = -Infinity;
|
|
54
|
+
return () => {
|
|
55
|
+
if (token && now() - checkedAt < ttl) return token;
|
|
56
|
+
checkedAt = now();
|
|
57
|
+
try {
|
|
58
|
+
const st = (0, import_node_fs.statSync)(file);
|
|
59
|
+
const next = `${st.mtimeMs}:${st.size}`;
|
|
60
|
+
if (!token || next !== stamp) {
|
|
61
|
+
const text = (0, import_node_fs.readFileSync)(file, "utf8").trim();
|
|
62
|
+
if (text) {
|
|
63
|
+
token = text;
|
|
64
|
+
stamp = next;
|
|
65
|
+
} else if (!token) {
|
|
66
|
+
throw new Error("token file is empty");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (!token) throw new Error(`Cannot read the storefront token file ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
}
|
|
72
|
+
return token;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function storefrontTokenFromEnv(env = process.env, options = {}) {
|
|
76
|
+
const file = env.STOREFRONT_TOKEN_FILE?.trim();
|
|
77
|
+
if (file) return createFileTokenSource(file, options);
|
|
78
|
+
const dir = env.CREDENTIALS_DIRECTORY?.trim();
|
|
79
|
+
if (dir) {
|
|
80
|
+
const f = (0, import_node_path.join)(dir, "storefront_token");
|
|
81
|
+
if ((0, import_node_fs.existsSync)(f)) return createFileTokenSource(f, options);
|
|
82
|
+
}
|
|
83
|
+
const plain = env.STOREFRONT_TOKEN?.trim();
|
|
84
|
+
return plain || void 0;
|
|
85
|
+
}
|
|
86
|
+
function writeFileAtomic(file, content, mode = 384) {
|
|
87
|
+
const dir = (0, import_node_path.dirname)(file);
|
|
88
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
89
|
+
const tmp = (0, import_node_path.join)(dir, `.${file.split("/").pop()}.${process.pid}.${Date.now().toString(36)}.tmp`);
|
|
90
|
+
const fd = (0, import_node_fs.openSync)(tmp, "wx", mode);
|
|
91
|
+
try {
|
|
92
|
+
(0, import_node_fs.writeSync)(fd, content);
|
|
93
|
+
(0, import_node_fs.fsyncSync)(fd);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
(0, import_node_fs.closeSync)(fd);
|
|
96
|
+
try {
|
|
97
|
+
(0, import_node_fs.unlinkSync)(tmp);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
(0, import_node_fs.closeSync)(fd);
|
|
103
|
+
try {
|
|
104
|
+
(0, import_node_fs.chmodSync)(tmp, mode);
|
|
105
|
+
(0, import_node_fs.renameSync)(tmp, file);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
try {
|
|
108
|
+
(0, import_node_fs.unlinkSync)(tmp);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
var ConnectError = class extends Error {
|
|
115
|
+
constructor(kind, message, status = 0) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.kind = kind;
|
|
118
|
+
this.status = status;
|
|
119
|
+
}
|
|
120
|
+
kind;
|
|
121
|
+
status;
|
|
122
|
+
name = "ConnectError";
|
|
123
|
+
};
|
|
124
|
+
var trimSlash = (u) => u.replace(/\/+$/, "");
|
|
125
|
+
async function post(fetchImpl, url, body, bearer) {
|
|
126
|
+
let res;
|
|
127
|
+
try {
|
|
128
|
+
res = await fetchImpl(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...bearer ? { Authorization: `Bearer ${bearer}` } : {} },
|
|
131
|
+
body: JSON.stringify(body),
|
|
132
|
+
signal: AbortSignal.timeout(15e3)
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
|
|
136
|
+
}
|
|
137
|
+
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;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(text);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function assertRedeem(r) {
|
|
152
|
+
const o = r;
|
|
153
|
+
if (!o || typeof o.secret_token !== "string" || !o.secret_token || typeof o.connection_id !== "string" || !Array.isArray(o.scopes)) {
|
|
154
|
+
throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function redeemConnectionCode(opts) {
|
|
158
|
+
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);
|
|
162
|
+
return res;
|
|
163
|
+
}
|
|
164
|
+
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 };
|
|
167
|
+
writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
|
|
168
|
+
writeFileAtomic(tokenFile, r.secret_token + "\n");
|
|
169
|
+
}
|
|
170
|
+
function loadConnectionState(tokenFile) {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse((0, import_node_fs.readFileSync)(stateFileFor(tokenFile), "utf8"));
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function connectStore(options = {}) {
|
|
178
|
+
const tokenFile = (0, import_node_path.resolve)(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? "storefront_token");
|
|
179
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
180
|
+
const now = options.now ?? Date.now;
|
|
181
|
+
const report = (m) => {
|
|
182
|
+
try {
|
|
183
|
+
options.onError?.(m);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
async function tick() {
|
|
188
|
+
const state = loadConnectionState(tokenFile);
|
|
189
|
+
const opsUrl = options.opsUrl ?? state?.ops_url;
|
|
190
|
+
let token;
|
|
191
|
+
try {
|
|
192
|
+
token = (0, import_node_fs.readFileSync)(tokenFile, "utf8").trim();
|
|
193
|
+
} catch {
|
|
194
|
+
report(`No se pudo leer el archivo del token (${tokenFile}).`);
|
|
195
|
+
return "failed";
|
|
196
|
+
}
|
|
197
|
+
if (!opsUrl || !token) {
|
|
198
|
+
report("Falta la direcci\xF3n de OPS o el token: vuelve a ejecutar `npx @asteby/storefront connect`.");
|
|
199
|
+
return "failed";
|
|
200
|
+
}
|
|
201
|
+
const base = `${trimSlash(opsUrl)}/api/connect/v1`;
|
|
202
|
+
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) {
|
|
206
|
+
const next = await post(fetchImpl, `${base}/rotate`, {}, token);
|
|
207
|
+
assertRedeem(next);
|
|
208
|
+
saveConnection(tokenFile, { ...next, ops_url: next.ops_url || opsUrl }, opsUrl);
|
|
209
|
+
return "rotated";
|
|
210
|
+
}
|
|
211
|
+
return "ok";
|
|
212
|
+
} catch (err) {
|
|
213
|
+
report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Fall\xF3 el latido de la conexi\xF3n (${maskToken(token)}).`);
|
|
214
|
+
return "failed";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
tokenFile,
|
|
219
|
+
/** One heartbeat (+ rotation when due). Resolves, never rejects. */
|
|
220
|
+
tick,
|
|
221
|
+
/** Starts the periodic heartbeat; returns `stop()`. */
|
|
222
|
+
keepFresh(opts = {}) {
|
|
223
|
+
const timer = setInterval(() => void tick(), opts.intervalMs ?? 6e5);
|
|
224
|
+
timer.unref?.();
|
|
225
|
+
void tick();
|
|
226
|
+
return () => clearInterval(timer);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
231
|
+
0 && (module.exports = {
|
|
232
|
+
ConnectError,
|
|
233
|
+
SDK_VERSION,
|
|
234
|
+
connectStore,
|
|
235
|
+
createFileTokenSource,
|
|
236
|
+
loadConnectionState,
|
|
237
|
+
redeemConnectionCode,
|
|
238
|
+
saveConnection,
|
|
239
|
+
stateFileFor,
|
|
240
|
+
storefrontTokenFromEnv,
|
|
241
|
+
writeFileAtomic
|
|
242
|
+
});
|
|
243
|
+
//# sourceMappingURL=node.cjs.map
|
|
@@ -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\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":[]}
|
package/dist/node.d.cts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
|
|
3
|
+
declare const SDK_VERSION: string;
|
|
4
|
+
type FileTokenSourceOptions = {
|
|
5
|
+
/** How often the file is checked again (default 30 s). Between checks the cached token is returned. */
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
/** Clock, for tests. */
|
|
8
|
+
now?: () => number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A `() => string` for `createStorefrontClient({ token })` that reads `file`, caches it and looks again every
|
|
12
|
+
* `ttlMs` (30 s): the file is read only when its mtime or size changed. A failed re-read keeps the last good token
|
|
13
|
+
* (a rotation in progress never takes the store down); only a first read that fails throws.
|
|
14
|
+
*/
|
|
15
|
+
declare function createFileTokenSource(file: string, options?: FileTokenSourceOptions): () => string;
|
|
16
|
+
type TokenEnv = Record<string, string | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* The store's token from the environment, in this order:
|
|
19
|
+
* 1. `STOREFRONT_TOKEN_FILE` (path),
|
|
20
|
+
* 2. `$CREDENTIALS_DIRECTORY/storefront_token` (systemd `LoadCredential=`),
|
|
21
|
+
* 3. `STOREFRONT_TOKEN` (the plain variable, as before).
|
|
22
|
+
* Files come back as a rotating source (re-read every 30 s); the variable as a plain string. `undefined` when
|
|
23
|
+
* nothing is configured.
|
|
24
|
+
*/
|
|
25
|
+
declare function storefrontTokenFromEnv(env?: TokenEnv, options?: FileTokenSourceOptions): string | (() => string) | undefined;
|
|
26
|
+
/** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */
|
|
27
|
+
declare function writeFileAtomic(file: string, content: string, mode?: number): void;
|
|
28
|
+
type RedeemResponse = {
|
|
29
|
+
ops_url: string;
|
|
30
|
+
connection_id: string;
|
|
31
|
+
environment: string;
|
|
32
|
+
secret_token: string;
|
|
33
|
+
publishable_token?: string;
|
|
34
|
+
scopes: string[];
|
|
35
|
+
/** ISO time from which the token should be rotated. */
|
|
36
|
+
rotate_after: string;
|
|
37
|
+
};
|
|
38
|
+
type ConnectClientInfo = {
|
|
39
|
+
sdk_version: string;
|
|
40
|
+
hostname: string;
|
|
41
|
+
site_url?: string;
|
|
42
|
+
};
|
|
43
|
+
/** Why a connection call failed, in terms an operator can act on. */
|
|
44
|
+
type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response';
|
|
45
|
+
declare class ConnectError extends Error {
|
|
46
|
+
readonly kind: ConnectErrorKind;
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly name = "ConnectError";
|
|
49
|
+
constructor(kind: ConnectErrorKind, message: string, status?: number);
|
|
50
|
+
}
|
|
51
|
+
type RedeemOptions = {
|
|
52
|
+
opsUrl: string;
|
|
53
|
+
code: string;
|
|
54
|
+
siteUrl?: string;
|
|
55
|
+
fetch?: FetchLike;
|
|
56
|
+
};
|
|
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>;
|
|
59
|
+
/** What is kept next to the token file: everything but the secret. */
|
|
60
|
+
type ConnectionState = {
|
|
61
|
+
ops_url: string;
|
|
62
|
+
connection_id: string;
|
|
63
|
+
environment: string;
|
|
64
|
+
scopes: string[];
|
|
65
|
+
rotate_after: string;
|
|
66
|
+
};
|
|
67
|
+
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;
|
|
70
|
+
declare function loadConnectionState(tokenFile: string): ConnectionState | null;
|
|
71
|
+
type ConnectStoreOptions = {
|
|
72
|
+
/** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */
|
|
73
|
+
opsUrl?: string;
|
|
74
|
+
/** File `connect` wrote (default: `STOREFRONT_TOKEN_FILE`, then `./storefront_token`). */
|
|
75
|
+
tokenFile?: string;
|
|
76
|
+
fetch?: FetchLike;
|
|
77
|
+
/** Clock, for tests. */
|
|
78
|
+
now?: () => number;
|
|
79
|
+
/** Called with a short message when a heartbeat or rotation fails. It never receives a token. */
|
|
80
|
+
onError?: (message: string) => void;
|
|
81
|
+
};
|
|
82
|
+
type KeepFreshOptions = {
|
|
83
|
+
/** Heartbeat period (default 10 min). */
|
|
84
|
+
intervalMs?: number;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
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.
|
|
91
|
+
*/
|
|
92
|
+
declare function connectStore(options?: ConnectStoreOptions): {
|
|
93
|
+
tokenFile: string;
|
|
94
|
+
/** One heartbeat (+ rotation when due). Resolves, never rejects. */
|
|
95
|
+
tick: () => Promise<"ok" | "rotated" | "failed">;
|
|
96
|
+
/** Starts the periodic heartbeat; returns `stop()`. */
|
|
97
|
+
keepFresh(opts?: KeepFreshOptions): () => void;
|
|
98
|
+
};
|
|
99
|
+
|
|
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 };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
|
|
3
|
+
declare const SDK_VERSION: string;
|
|
4
|
+
type FileTokenSourceOptions = {
|
|
5
|
+
/** How often the file is checked again (default 30 s). Between checks the cached token is returned. */
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
/** Clock, for tests. */
|
|
8
|
+
now?: () => number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A `() => string` for `createStorefrontClient({ token })` that reads `file`, caches it and looks again every
|
|
12
|
+
* `ttlMs` (30 s): the file is read only when its mtime or size changed. A failed re-read keeps the last good token
|
|
13
|
+
* (a rotation in progress never takes the store down); only a first read that fails throws.
|
|
14
|
+
*/
|
|
15
|
+
declare function createFileTokenSource(file: string, options?: FileTokenSourceOptions): () => string;
|
|
16
|
+
type TokenEnv = Record<string, string | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* The store's token from the environment, in this order:
|
|
19
|
+
* 1. `STOREFRONT_TOKEN_FILE` (path),
|
|
20
|
+
* 2. `$CREDENTIALS_DIRECTORY/storefront_token` (systemd `LoadCredential=`),
|
|
21
|
+
* 3. `STOREFRONT_TOKEN` (the plain variable, as before).
|
|
22
|
+
* Files come back as a rotating source (re-read every 30 s); the variable as a plain string. `undefined` when
|
|
23
|
+
* nothing is configured.
|
|
24
|
+
*/
|
|
25
|
+
declare function storefrontTokenFromEnv(env?: TokenEnv, options?: FileTokenSourceOptions): string | (() => string) | undefined;
|
|
26
|
+
/** Writes `content` to `file` atomically (temp file in the same directory, fsync, rename) with mode 0600. */
|
|
27
|
+
declare function writeFileAtomic(file: string, content: string, mode?: number): void;
|
|
28
|
+
type RedeemResponse = {
|
|
29
|
+
ops_url: string;
|
|
30
|
+
connection_id: string;
|
|
31
|
+
environment: string;
|
|
32
|
+
secret_token: string;
|
|
33
|
+
publishable_token?: string;
|
|
34
|
+
scopes: string[];
|
|
35
|
+
/** ISO time from which the token should be rotated. */
|
|
36
|
+
rotate_after: string;
|
|
37
|
+
};
|
|
38
|
+
type ConnectClientInfo = {
|
|
39
|
+
sdk_version: string;
|
|
40
|
+
hostname: string;
|
|
41
|
+
site_url?: string;
|
|
42
|
+
};
|
|
43
|
+
/** Why a connection call failed, in terms an operator can act on. */
|
|
44
|
+
type ConnectErrorKind = 'invalid_code' | 'rate_limited' | 'network' | 'unauthorized' | 'server' | 'invalid_response';
|
|
45
|
+
declare class ConnectError extends Error {
|
|
46
|
+
readonly kind: ConnectErrorKind;
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly name = "ConnectError";
|
|
49
|
+
constructor(kind: ConnectErrorKind, message: string, status?: number);
|
|
50
|
+
}
|
|
51
|
+
type RedeemOptions = {
|
|
52
|
+
opsUrl: string;
|
|
53
|
+
code: string;
|
|
54
|
+
siteUrl?: string;
|
|
55
|
+
fetch?: FetchLike;
|
|
56
|
+
};
|
|
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>;
|
|
59
|
+
/** What is kept next to the token file: everything but the secret. */
|
|
60
|
+
type ConnectionState = {
|
|
61
|
+
ops_url: string;
|
|
62
|
+
connection_id: string;
|
|
63
|
+
environment: string;
|
|
64
|
+
scopes: string[];
|
|
65
|
+
rotate_after: string;
|
|
66
|
+
};
|
|
67
|
+
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;
|
|
70
|
+
declare function loadConnectionState(tokenFile: string): ConnectionState | null;
|
|
71
|
+
type ConnectStoreOptions = {
|
|
72
|
+
/** OPS base URL (`https://ops.example.com`). Defaults to the one saved by `connect`. */
|
|
73
|
+
opsUrl?: string;
|
|
74
|
+
/** File `connect` wrote (default: `STOREFRONT_TOKEN_FILE`, then `./storefront_token`). */
|
|
75
|
+
tokenFile?: string;
|
|
76
|
+
fetch?: FetchLike;
|
|
77
|
+
/** Clock, for tests. */
|
|
78
|
+
now?: () => number;
|
|
79
|
+
/** Called with a short message when a heartbeat or rotation fails. It never receives a token. */
|
|
80
|
+
onError?: (message: string) => void;
|
|
81
|
+
};
|
|
82
|
+
type KeepFreshOptions = {
|
|
83
|
+
/** Heartbeat period (default 10 min). */
|
|
84
|
+
intervalMs?: number;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
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.
|
|
91
|
+
*/
|
|
92
|
+
declare function connectStore(options?: ConnectStoreOptions): {
|
|
93
|
+
tokenFile: string;
|
|
94
|
+
/** One heartbeat (+ rotation when due). Resolves, never rejects. */
|
|
95
|
+
tick: () => Promise<"ok" | "rotated" | "failed">;
|
|
96
|
+
/** Starts the periodic heartbeat; returns `stop()`. */
|
|
97
|
+
keepFresh(opts?: KeepFreshOptions): () => void;
|
|
98
|
+
};
|
|
99
|
+
|
|
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 };
|