@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/node.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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.0" : "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
|
+
var ConnectError = class extends Error {
|
|
82
|
+
constructor(kind, message, status = 0) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.kind = kind;
|
|
85
|
+
this.status = status;
|
|
86
|
+
}
|
|
87
|
+
kind;
|
|
88
|
+
status;
|
|
89
|
+
name = "ConnectError";
|
|
90
|
+
};
|
|
91
|
+
var trimSlash = (u) => u.replace(/\/+$/, "");
|
|
92
|
+
async function post(fetchImpl, url, body, bearer) {
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetchImpl(url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...bearer ? { Authorization: `Bearer ${bearer}` } : {} },
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
signal: AbortSignal.timeout(15e3)
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
throw new ConnectError("network", "No pudimos comunicarnos con OPS. Revisa la direcci\xF3n y tu conexi\xF3n a internet.");
|
|
103
|
+
}
|
|
104
|
+
const text = await res.text().catch(() => "");
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
if (res.status === 429) throw new ConnectError("rate_limited", "Demasiados intentos. Espera unos minutos e int\xE9ntalo de nuevo.", 429);
|
|
107
|
+
if (res.status === 401 || res.status === 403) throw new ConnectError("unauthorized", "OPS rechaz\xF3 la credencial de esta tienda.", res.status);
|
|
108
|
+
if (res.status >= 500) throw new ConnectError("server", "OPS no pudo atender la solicitud. Int\xE9ntalo de nuevo en un momento.", res.status);
|
|
109
|
+
throw new ConnectError("invalid_code", "El c\xF3digo no es v\xE1lido, ya se us\xF3 o venci\xF3. Genera uno nuevo en OPS \u2192 Conexiones y API.", res.status);
|
|
110
|
+
}
|
|
111
|
+
if (!text) return void 0;
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(text);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.", res.status);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function assertRedeem(r) {
|
|
119
|
+
const o = r;
|
|
120
|
+
if (!o || typeof o.secret_token !== "string" || !o.secret_token || typeof o.connection_id !== "string" || !Array.isArray(o.scopes)) {
|
|
121
|
+
throw new ConnectError("invalid_response", "OPS respondi\xF3 algo que no entendimos.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function redeemConnectionCode(opts) {
|
|
125
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
126
|
+
const client_info = { sdk_version: SDK_VERSION, hostname: hostname(), ...opts.siteUrl ? { site_url: opts.siteUrl } : {} };
|
|
127
|
+
const res = await post(fetchImpl, `${trimSlash(opts.opsUrl)}/api/connect/v1/redeem`, { code: opts.code.trim(), client_info });
|
|
128
|
+
assertRedeem(res);
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
var stateFileFor = (tokenFile) => `${tokenFile}.json`;
|
|
132
|
+
function saveConnection(tokenFile, r, opsUrl = r.ops_url) {
|
|
133
|
+
const state = { ops_url: trimSlash(r.ops_url || opsUrl), connection_id: r.connection_id, environment: r.environment, scopes: r.scopes, rotate_after: r.rotate_after };
|
|
134
|
+
writeFileAtomic(stateFileFor(tokenFile), JSON.stringify(state, null, 2) + "\n");
|
|
135
|
+
writeFileAtomic(tokenFile, r.secret_token + "\n");
|
|
136
|
+
}
|
|
137
|
+
function loadConnectionState(tokenFile) {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(readFileSync(stateFileFor(tokenFile), "utf8"));
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function connectStore(options = {}) {
|
|
145
|
+
const tokenFile = resolve(options.tokenFile ?? process.env.STOREFRONT_TOKEN_FILE ?? "storefront_token");
|
|
146
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
147
|
+
const now = options.now ?? Date.now;
|
|
148
|
+
const report = (m) => {
|
|
149
|
+
try {
|
|
150
|
+
options.onError?.(m);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
async function tick() {
|
|
155
|
+
const state = loadConnectionState(tokenFile);
|
|
156
|
+
const opsUrl = options.opsUrl ?? state?.ops_url;
|
|
157
|
+
let token;
|
|
158
|
+
try {
|
|
159
|
+
token = readFileSync(tokenFile, "utf8").trim();
|
|
160
|
+
} catch {
|
|
161
|
+
report(`No se pudo leer el archivo del token (${tokenFile}).`);
|
|
162
|
+
return "failed";
|
|
163
|
+
}
|
|
164
|
+
if (!opsUrl || !token) {
|
|
165
|
+
report("Falta la direcci\xF3n de OPS o el token: vuelve a ejecutar `npx @asteby/storefront connect`.");
|
|
166
|
+
return "failed";
|
|
167
|
+
}
|
|
168
|
+
const base = `${trimSlash(opsUrl)}/api/connect/v1`;
|
|
169
|
+
try {
|
|
170
|
+
await post(fetchImpl, `${base}/heartbeat`, { sdk_version: SDK_VERSION, hostname: hostname() }, token);
|
|
171
|
+
const due = state?.rotate_after ? Date.parse(state.rotate_after) : NaN;
|
|
172
|
+
if (!Number.isNaN(due) && now() >= due) {
|
|
173
|
+
const next = await post(fetchImpl, `${base}/rotate`, {}, token);
|
|
174
|
+
assertRedeem(next);
|
|
175
|
+
saveConnection(tokenFile, { ...next, ops_url: next.ops_url || opsUrl }, opsUrl);
|
|
176
|
+
return "rotated";
|
|
177
|
+
}
|
|
178
|
+
return "ok";
|
|
179
|
+
} catch (err) {
|
|
180
|
+
report(err instanceof ConnectError ? `${err.message} (${maskToken(token)})` : `Fall\xF3 el latido de la conexi\xF3n (${maskToken(token)}).`);
|
|
181
|
+
return "failed";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
tokenFile,
|
|
186
|
+
/** One heartbeat (+ rotation when due). Resolves, never rejects. */
|
|
187
|
+
tick,
|
|
188
|
+
/** Starts the periodic heartbeat; returns `stop()`. */
|
|
189
|
+
keepFresh(opts = {}) {
|
|
190
|
+
const timer = setInterval(() => void tick(), opts.intervalMs ?? 6e5);
|
|
191
|
+
timer.unref?.();
|
|
192
|
+
void tick();
|
|
193
|
+
return () => clearInterval(timer);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
ConnectError,
|
|
199
|
+
SDK_VERSION,
|
|
200
|
+
connectStore,
|
|
201
|
+
createFileTokenSource,
|
|
202
|
+
loadConnectionState,
|
|
203
|
+
redeemConnectionCode,
|
|
204
|
+
saveConnection,
|
|
205
|
+
stateFileFor,
|
|
206
|
+
storefrontTokenFromEnv,
|
|
207
|
+
writeFileAtomic
|
|
208
|
+
};
|
|
209
|
+
//# 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\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":[]}
|
|
@@ -318,6 +318,8 @@ type Cart = {
|
|
|
318
318
|
note?: string | null;
|
|
319
319
|
/** Automatic-discount tiers and free-shipping progress (contract 1.2); `null`/absent without the promotions addon. */
|
|
320
320
|
promotions?: CartPromotions | null;
|
|
321
|
+
/** Invoice the buyer asked for (`carts.setInvoice`); `null`/absent when none. */
|
|
322
|
+
invoice?: CartInvoice | null;
|
|
321
323
|
updated_at: string;
|
|
322
324
|
};
|
|
323
325
|
type CartLineInput = {
|
|
@@ -395,20 +397,31 @@ type DeliveryInput = {
|
|
|
395
397
|
rate_id?: string;
|
|
396
398
|
branch_id?: string;
|
|
397
399
|
};
|
|
400
|
+
/**
|
|
401
|
+
* Fiscal data the buyer asks an invoice for. The Spanish aliases are still accepted by the API.
|
|
402
|
+
* `document` is the evidence of where the data came from (never the document itself).
|
|
403
|
+
*/
|
|
398
404
|
type InvoiceRequest = {
|
|
399
405
|
requested?: boolean;
|
|
400
406
|
tax_id?: string;
|
|
401
407
|
legal_name?: string;
|
|
402
408
|
tax_regime?: string;
|
|
403
|
-
/**
|
|
409
|
+
/** Invoice use; the region says which are allowed for `tax_regime`. */
|
|
404
410
|
use?: string;
|
|
405
411
|
postal_code?: string;
|
|
412
|
+
/** Defaults to the market's country. */
|
|
413
|
+
country_code?: string;
|
|
414
|
+
document?: FiscalDocumentEvidence;
|
|
406
415
|
/** Spanish aliases the API also accepts. */
|
|
407
416
|
rfc?: string;
|
|
408
417
|
razon_social?: string;
|
|
409
418
|
regimen?: string;
|
|
410
419
|
uso_cfdi?: string;
|
|
411
420
|
cp?: string;
|
|
421
|
+
/** Alias of `use`. */
|
|
422
|
+
invoice_use?: string;
|
|
423
|
+
/** Also save the data to the buyer's account (needs a session). */
|
|
424
|
+
save?: boolean;
|
|
412
425
|
[key: string]: unknown;
|
|
413
426
|
};
|
|
414
427
|
type CheckoutInput = {
|
|
@@ -472,6 +485,8 @@ type Customer = {
|
|
|
472
485
|
first_name: string | null;
|
|
473
486
|
last_name: string | null;
|
|
474
487
|
default_address_id: string | null;
|
|
488
|
+
/** Saved fiscal data; `null`/absent when none. */
|
|
489
|
+
fiscal?: FiscalIdentity | null;
|
|
475
490
|
created_at: string;
|
|
476
491
|
};
|
|
477
492
|
type CustomerIdentifier = {
|
|
@@ -489,6 +504,65 @@ type CustomerSession = {
|
|
|
489
504
|
expires_at: string;
|
|
490
505
|
customer: Customer;
|
|
491
506
|
};
|
|
507
|
+
/** What the store's buyer accounts support (`GET /storefront/customer/capabilities`). Cache briefly, not forever. */
|
|
508
|
+
type CustomerCapabilities = {
|
|
509
|
+
/** Email + password registration and login are enabled. */
|
|
510
|
+
password: boolean;
|
|
511
|
+
/** Registration needs an emailed code (`customer.verifyRegistration`) before the account exists. */
|
|
512
|
+
email_verification: boolean;
|
|
513
|
+
/** Channels `customer.requestOtp` can deliver the code through. */
|
|
514
|
+
otp_channels: ('email' | 'whatsapp')[];
|
|
515
|
+
/** What invoicing the store offers. Absent on older servers: treat as `available: false`. */
|
|
516
|
+
invoicing?: InvoicingCapabilities;
|
|
517
|
+
/**
|
|
518
|
+
* Why a way of signing in is off. `missing_scope`: the store's token lacks a scope (see `missing_scopes`), so
|
|
519
|
+
* the store is misconfigured; it is not a choice of the merchant. Absent when everything is as configured.
|
|
520
|
+
*/
|
|
521
|
+
unavailable_reason?: 'missing_scope' | (string & {});
|
|
522
|
+
/** Scopes the token lacks (e.g. `customer:write`) when `unavailable_reason` is `missing_scope`. */
|
|
523
|
+
missing_scopes?: string[];
|
|
524
|
+
};
|
|
525
|
+
/** `GET {gateway}/_token`: what the token in use is and what it can do. Needs no scope. */
|
|
526
|
+
type TokenIntrospection = {
|
|
527
|
+
kind: string;
|
|
528
|
+
environment: 'sandbox' | 'production' | (string & {});
|
|
529
|
+
scopes: string[];
|
|
530
|
+
/** Scopes the connection should have but the token lacks. Empty when healthy. */
|
|
531
|
+
missing_scopes: string[];
|
|
532
|
+
connection_id: string | null;
|
|
533
|
+
expires_at: string | null;
|
|
534
|
+
};
|
|
535
|
+
type CustomerRegisterInput = {
|
|
536
|
+
email: string;
|
|
537
|
+
password: string;
|
|
538
|
+
first_name?: string;
|
|
539
|
+
last_name?: string;
|
|
540
|
+
};
|
|
541
|
+
/** `verification_required`: a code was emailed, finish with `customer.verifyRegistration`. `created`: the account is ready and signed in. */
|
|
542
|
+
type CustomerRegisterResult = {
|
|
543
|
+
status: 'verification_required';
|
|
544
|
+
} | {
|
|
545
|
+
status: 'created';
|
|
546
|
+
session: CustomerSession;
|
|
547
|
+
};
|
|
548
|
+
type CustomerVerifyRegistrationInput = {
|
|
549
|
+
email: string;
|
|
550
|
+
code: string;
|
|
551
|
+
};
|
|
552
|
+
type CustomerPasswordLoginInput = {
|
|
553
|
+
email: string;
|
|
554
|
+
password: string;
|
|
555
|
+
};
|
|
556
|
+
/** `current_password` is required by the server when the account already has a password. */
|
|
557
|
+
type CustomerSetPasswordInput = {
|
|
558
|
+
password: string;
|
|
559
|
+
current_password?: string;
|
|
560
|
+
};
|
|
561
|
+
type CustomerResetPasswordInput = {
|
|
562
|
+
email: string;
|
|
563
|
+
code: string;
|
|
564
|
+
password: string;
|
|
565
|
+
};
|
|
492
566
|
type OrderLine = {
|
|
493
567
|
title: string;
|
|
494
568
|
variant_title: string | null;
|
|
@@ -505,6 +579,8 @@ type Order = {
|
|
|
505
579
|
created_at: string;
|
|
506
580
|
total: Money;
|
|
507
581
|
lines: OrderLine[];
|
|
582
|
+
/** Invoice state; absent on older servers. */
|
|
583
|
+
invoice?: InvoiceSummary;
|
|
508
584
|
};
|
|
509
585
|
type ReturnRequestInput = {
|
|
510
586
|
lines: {
|
|
@@ -518,9 +594,12 @@ type ReturnRequest = {
|
|
|
518
594
|
order_id: string;
|
|
519
595
|
status: string;
|
|
520
596
|
};
|
|
597
|
+
/** `200` issued, `202` pending. The 422 / 409 / 503 outcomes arrive as a `StorefrontError`. */
|
|
521
598
|
type InvoiceResult = {
|
|
522
599
|
status: string;
|
|
523
600
|
url?: string | null;
|
|
601
|
+
stage?: InvoiceStage;
|
|
602
|
+
invoice?: InvoiceSummary;
|
|
524
603
|
};
|
|
525
604
|
type Theme = {
|
|
526
605
|
/** Design tokens, e.g. `{ primary: '#0f766e', 'primary-foreground': '#ffffff' }`. */
|
|
@@ -562,7 +641,9 @@ type Article = {
|
|
|
562
641
|
published_at: string;
|
|
563
642
|
seo: SEO;
|
|
564
643
|
};
|
|
565
|
-
type StorefrontErrorCode = 'validation' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'insufficient_stock' | 'price_mismatch' | 'invalid_state' | 'rate_limited' | 'not_modified' | 'server_error' | 'network' | 'timeout' | 'aborted' | 'invalid_response'
|
|
644
|
+
type StorefrontErrorCode = 'validation' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'insufficient_stock' | 'price_mismatch' | 'invalid_state' | 'rate_limited' | 'not_modified' | 'server_error' | 'network' | 'timeout' | 'aborted' | 'invalid_response'
|
|
645
|
+
/** `403`: the token lacks a scope the route needs. `StorefrontError.requiredScopes` lists them. */
|
|
646
|
+
| 'insufficient_scope' | (string & {});
|
|
566
647
|
/** One affected line of a `409 insufficient_stock`. `line_id` is `null` when the line did not exist yet (add). */
|
|
567
648
|
type InsufficientStockLine = {
|
|
568
649
|
line_id: string | null;
|
|
@@ -580,6 +661,7 @@ type StorefrontErrorBody = {
|
|
|
580
661
|
message: string;
|
|
581
662
|
field?: string;
|
|
582
663
|
details?: unknown;
|
|
664
|
+
required_scopes?: string[];
|
|
583
665
|
};
|
|
584
666
|
};
|
|
585
667
|
type RestockSubscriptionInput = {
|
|
@@ -831,5 +913,253 @@ type PromotionsList = {
|
|
|
831
913
|
nodes: unknown[];
|
|
832
914
|
announcements: PromotionAnnouncement[];
|
|
833
915
|
};
|
|
916
|
+
/** Where an invoice is. `needs_data`: the buyer must fix something. `needs_review`: the store must act. */
|
|
917
|
+
type InvoiceStage = 'not_requested' | 'requested' | 'awaiting_payment' | 'queued' | 'issuing' | 'issued' | 'cancelled' | 'needs_data' | 'retrying' | 'needs_review' | 'window_closed' | (string & {});
|
|
918
|
+
/** What the buyer can do next about an invoice. */
|
|
919
|
+
type InvoiceAction = 'none' | 'request' | 'wait' | 'correct_data' | 'download' | 'contact_store' | (string & {});
|
|
920
|
+
type InvoiceSummary = {
|
|
921
|
+
/** Single-word status (`requested`, `issued`...). Prefer `stage`. */
|
|
922
|
+
status: string;
|
|
923
|
+
stage?: InvoiceStage;
|
|
924
|
+
requested?: boolean;
|
|
925
|
+
/** Machine-readable cause when `stage` is `needs_data` / `needs_review` (e.g. `invoice.tax_regime`). */
|
|
926
|
+
reason_code?: string | null;
|
|
927
|
+
action?: InvoiceAction;
|
|
928
|
+
/** The buyer may ask for it now. */
|
|
929
|
+
can_request?: boolean;
|
|
930
|
+
/** Why `can_request` is `false` (e.g. `awaiting_payment`, `window_closed`, `not_invoiceable`). */
|
|
931
|
+
blocked_reason?: string | null;
|
|
932
|
+
/** Fiscal folio (UUID in Mexico) once issued. */
|
|
933
|
+
folio?: string | null;
|
|
934
|
+
/** Signed, short-lived download links. */
|
|
935
|
+
url?: string | null;
|
|
936
|
+
xml_url?: string | null;
|
|
937
|
+
};
|
|
938
|
+
/** The invoice request of a cart (`Cart.invoice`). The tax id is masked: the full value is never echoed back. */
|
|
939
|
+
type CartInvoice = {
|
|
940
|
+
requested: boolean;
|
|
941
|
+
tax_id_masked?: string | null;
|
|
942
|
+
legal_name?: string | null;
|
|
943
|
+
country_code?: string | null;
|
|
944
|
+
/** `IdentityDocumentSpec.kind` the data was read from, when it was. */
|
|
945
|
+
document_kind?: string | null;
|
|
946
|
+
};
|
|
947
|
+
type InvoicingCapabilities = {
|
|
948
|
+
available: boolean;
|
|
949
|
+
/** Countries (ISO 3166-1 alpha-2) the store can invoice for. */
|
|
950
|
+
countries: string[];
|
|
951
|
+
/** `optional`: the checkout offers "I need an invoice". `off`: invoices can only be requested afterwards. */
|
|
952
|
+
checkout: 'off' | 'optional';
|
|
953
|
+
/** Invoices can be requested after the purchase (`customer.requestInvoice`). */
|
|
954
|
+
post_purchase: boolean;
|
|
955
|
+
/** Days after the purchase in which an invoice can still be requested. */
|
|
956
|
+
window_days?: number;
|
|
957
|
+
/** `IdentityDocumentSpec.kind` values the buyer can upload to prefill the data. */
|
|
958
|
+
identity_documents: string[];
|
|
959
|
+
/** The store can check the data against the tax authority online (informative, never blocks). */
|
|
960
|
+
online_verify: boolean;
|
|
961
|
+
};
|
|
962
|
+
/** One regime or invoice use of the region's fiscal catalog. */
|
|
963
|
+
type RegionFiscalEntry = {
|
|
964
|
+
code: string;
|
|
965
|
+
label: string;
|
|
966
|
+
label_en?: string;
|
|
967
|
+
/** Region-defined (`individual`, `company`...); compare as opaque strings. */
|
|
968
|
+
person_types: string[];
|
|
969
|
+
/** Invoice uses only: the regimes that may use it. Absent: any. */
|
|
970
|
+
allowed_regimes?: string[];
|
|
971
|
+
/** Folded phrases that identify the entry in a document (`catalog_phrases`); without them the label is used. */
|
|
972
|
+
match_phrases?: string[];
|
|
973
|
+
};
|
|
974
|
+
/**
|
|
975
|
+
* A document the buyer can upload to prefill their fiscal data, with the rules to read it. Published by the
|
|
976
|
+
* region in `fiscal_id.documents[]` (texts already in the response locale); the SDK engine knows no country.
|
|
977
|
+
*/
|
|
978
|
+
type IdentityDocumentSpec = {
|
|
979
|
+
/** `<COUNTRY>:<kind>`. */
|
|
980
|
+
kind: string;
|
|
981
|
+
country_code?: string;
|
|
982
|
+
label: string;
|
|
983
|
+
/** Explanation for the buyer; absent when the region has none in the response language. */
|
|
984
|
+
help?: string;
|
|
985
|
+
/** Where the buyer gets the document from the authority. */
|
|
986
|
+
obtain_url?: string;
|
|
987
|
+
/** MIME types the document comes in (the SDK reads PDFs). */
|
|
988
|
+
accept: string[];
|
|
989
|
+
max_bytes: number;
|
|
990
|
+
max_pages?: number;
|
|
991
|
+
/** `rules@1` runs the generic engine on `rules`; `plugin:<id>` runs an extractor from `registerIdentityExtractor`. */
|
|
992
|
+
engine: 'rules@1' | `plugin:${string}` | (string & {});
|
|
993
|
+
/** The `rules@1` rules (the API publishes them as an object; JSON text is accepted too). */
|
|
994
|
+
rules?: IdentityRulesV1 | string | null;
|
|
995
|
+
/** How the server can check the document against the authority (e.g. `qr_validator`). Informative. */
|
|
996
|
+
verify_kind?: string;
|
|
997
|
+
/** Days after `issued_on` in which the document counts as fresh. */
|
|
998
|
+
freshness_days?: number;
|
|
999
|
+
/** Fields the extractor never returns (sensitive data the store has no use for). */
|
|
1000
|
+
discard_fields: string[];
|
|
1001
|
+
};
|
|
1002
|
+
/** Region fiscal identity (`Region.fiscal_id`). */
|
|
1003
|
+
type RegionFiscalId = {
|
|
1004
|
+
/** Name of the tax id (`RFC`, `EIN`...). */
|
|
1005
|
+
kind: string;
|
|
1006
|
+
label: string;
|
|
1007
|
+
/** Regular expression a valid tax id matches. */
|
|
1008
|
+
pattern?: string;
|
|
1009
|
+
/** Generic ids valid without a document (e.g. the national generic public id). */
|
|
1010
|
+
generic_ids: string[];
|
|
1011
|
+
regimes: RegionFiscalEntry[];
|
|
1012
|
+
invoice_uses: RegionFiscalEntry[];
|
|
1013
|
+
/** Documents the buyer can upload; absent on older servers. */
|
|
1014
|
+
documents?: IdentityDocumentSpec[];
|
|
1015
|
+
};
|
|
1016
|
+
type RegionPaymentMethod = {
|
|
1017
|
+
code: string;
|
|
1018
|
+
label: string;
|
|
1019
|
+
description?: string;
|
|
1020
|
+
kind: string;
|
|
1021
|
+
flow: string;
|
|
1022
|
+
providers: string[];
|
|
1023
|
+
installments: {
|
|
1024
|
+
months: number[];
|
|
1025
|
+
min_amount: Money | null;
|
|
1026
|
+
} | null;
|
|
1027
|
+
min_amount: Money | null;
|
|
1028
|
+
max_amount: Money | null;
|
|
1029
|
+
expires_hours?: number;
|
|
1030
|
+
icon?: string;
|
|
1031
|
+
/** Payment forms the invoice may report for this method, comma separated; the first is the default (e.g. `04,28`). */
|
|
1032
|
+
fiscal_payment_form?: string;
|
|
1033
|
+
};
|
|
1034
|
+
/** `GET /storefront/region`: how the market formats, taxes, addresses and invoices. */
|
|
1035
|
+
type Region = {
|
|
1036
|
+
country_code: string;
|
|
1037
|
+
name: string;
|
|
1038
|
+
locale: {
|
|
1039
|
+
default: string;
|
|
1040
|
+
available: string[];
|
|
1041
|
+
current: string;
|
|
1042
|
+
};
|
|
1043
|
+
timezone?: string;
|
|
1044
|
+
currency: Record<string, unknown>;
|
|
1045
|
+
formats?: Record<string, unknown>;
|
|
1046
|
+
tax: Record<string, unknown>;
|
|
1047
|
+
address?: {
|
|
1048
|
+
postal_code?: {
|
|
1049
|
+
pattern: string;
|
|
1050
|
+
example?: string;
|
|
1051
|
+
};
|
|
1052
|
+
[key: string]: unknown;
|
|
1053
|
+
};
|
|
1054
|
+
subdivisions?: Record<string, unknown>[];
|
|
1055
|
+
phone?: Record<string, unknown>;
|
|
1056
|
+
fiscal_id?: RegionFiscalId;
|
|
1057
|
+
payment_methods?: RegionPaymentMethod[];
|
|
1058
|
+
};
|
|
1059
|
+
/** Where the fiscal data came from. `manual`: typed by the buyer. */
|
|
1060
|
+
type FiscalDocumentSource = 'pdf_client' | 'pdf_server' | 'qr' | 'manual';
|
|
1061
|
+
/** What is kept about the document the data was read from. Never the document itself. */
|
|
1062
|
+
type FiscalDocumentEvidence = {
|
|
1063
|
+
/** `IdentityDocumentSpec.kind`. */
|
|
1064
|
+
kind: string;
|
|
1065
|
+
/** The authority's id of the document. */
|
|
1066
|
+
document_id?: string | null;
|
|
1067
|
+
/** ISO date the authority issued it. */
|
|
1068
|
+
issued_on?: string | null;
|
|
1069
|
+
/** SHA-256 of the file, lowercase hex. */
|
|
1070
|
+
sha256?: string | null;
|
|
1071
|
+
/** Engine that read it, e.g. `rules@1`. */
|
|
1072
|
+
extractor?: string;
|
|
1073
|
+
source: FiscalDocumentSource;
|
|
1074
|
+
/** The QR agreed with the text: `true`/`false`, `null` when there was no QR to compare. */
|
|
1075
|
+
qr_match?: boolean | null;
|
|
1076
|
+
};
|
|
1077
|
+
type FiscalVerification = {
|
|
1078
|
+
/** `reviewed` (the buyer confirmed it), `verified`, `mismatch`, `unverified`, `stale`. */
|
|
1079
|
+
status: 'reviewed' | 'verified' | 'mismatch' | 'unverified' | 'stale' | (string & {});
|
|
1080
|
+
checked_at?: string | null;
|
|
1081
|
+
};
|
|
1082
|
+
/** Fiscal data as stored for a buyer. */
|
|
1083
|
+
type FiscalIdentity = {
|
|
1084
|
+
country_code: string;
|
|
1085
|
+
tax_id: string;
|
|
1086
|
+
legal_name: string;
|
|
1087
|
+
tax_regime: string;
|
|
1088
|
+
postal_code: string;
|
|
1089
|
+
invoice_use?: string | null;
|
|
1090
|
+
document?: FiscalDocumentEvidence | null;
|
|
1091
|
+
verification?: FiscalVerification | null;
|
|
1092
|
+
};
|
|
1093
|
+
type FiscalIdentityInput = Omit<FiscalIdentity, 'verification' | 'country_code'> & {
|
|
1094
|
+
country_code?: string;
|
|
1095
|
+
};
|
|
1096
|
+
/** `PATCH /customer`. `fiscal: null` deletes the saved fiscal data (ARCO). */
|
|
1097
|
+
type CustomerUpdateInput = {
|
|
1098
|
+
first_name?: string | null;
|
|
1099
|
+
last_name?: string | null;
|
|
1100
|
+
phone?: string | null;
|
|
1101
|
+
fiscal?: FiscalIdentityInput | null;
|
|
1102
|
+
};
|
|
1103
|
+
/** Post-processing: the value must match `pattern`; `group` of it replaces the value. */
|
|
1104
|
+
type IdentityCapture = {
|
|
1105
|
+
pattern: string;
|
|
1106
|
+
flags?: string;
|
|
1107
|
+
group?: number;
|
|
1108
|
+
};
|
|
1109
|
+
/**
|
|
1110
|
+
* One way to find a field. Only the members of its `strategy` apply:
|
|
1111
|
+
* `regex` (`pattern`, `flags`, `group`), `labeled_lines` (`label`), `concat_labeled` (`labels`, `separator`),
|
|
1112
|
+
* `catalog_phrases` (`catalog`, `start`, `end`), `date` (`pattern` with three groups, `flags`, `order`, `months`),
|
|
1113
|
+
* `qr_param` (`param`, `split`, `parts`, `match`). Any strategy takes `upper` and `capture`.
|
|
1114
|
+
*/
|
|
1115
|
+
type IdentityStrategy = {
|
|
1116
|
+
strategy: 'regex' | 'labeled_lines' | 'concat_labeled' | 'catalog_phrases' | 'date' | 'qr_param';
|
|
1117
|
+
pattern?: string;
|
|
1118
|
+
/** `i`, `s`, `m`. */
|
|
1119
|
+
flags?: string;
|
|
1120
|
+
group?: number;
|
|
1121
|
+
label?: string;
|
|
1122
|
+
labels?: string[];
|
|
1123
|
+
separator?: string;
|
|
1124
|
+
catalog?: string;
|
|
1125
|
+
start?: string;
|
|
1126
|
+
end?: string;
|
|
1127
|
+
/** Permutation of `d`, `m`, `y`: the meaning of the three groups. */
|
|
1128
|
+
order?: string;
|
|
1129
|
+
/** 12 month names, to read a month written in text. */
|
|
1130
|
+
months?: string[];
|
|
1131
|
+
/** `qr_param` fallback for a QR read by a keyboard scanner that garbled the punctuation: a pattern over the raw text whose group `group` is the value. Runs when `param` finds nothing. */
|
|
1132
|
+
scan?: string;
|
|
1133
|
+
param?: string;
|
|
1134
|
+
split?: string;
|
|
1135
|
+
parts?: number;
|
|
1136
|
+
match?: string;
|
|
1137
|
+
upper?: boolean;
|
|
1138
|
+
capture?: IdentityCapture;
|
|
1139
|
+
};
|
|
1140
|
+
type IdentityFieldRule = {
|
|
1141
|
+
/** Output name: `tax_id`, `legal_name`, `postal_code`, `tax_regime`, `document_id`, `issued_on`... */
|
|
1142
|
+
key: string;
|
|
1143
|
+
/** Listed in `missing` when not found. */
|
|
1144
|
+
required?: boolean;
|
|
1145
|
+
/** A list (e.g. every regime the document names). */
|
|
1146
|
+
multiple?: boolean;
|
|
1147
|
+
/** Tried in order; the first non-empty value wins. `qr_param` ones only fill what the text lacks. */
|
|
1148
|
+
strategies: IdentityStrategy[];
|
|
1149
|
+
};
|
|
1150
|
+
/**
|
|
1151
|
+
* `rules@1`, as the region publishes it (same semantics as the reference interpreter of the region packs). Text
|
|
1152
|
+
* is normalized (`nbsp`, `collapse_ws`), then each field is resolved. Patterns are in the subset common to Go
|
|
1153
|
+
* (RE2) and JavaScript: no lookaround, backreferences, named groups, inline flags or `\p`.
|
|
1154
|
+
*/
|
|
1155
|
+
type IdentityRulesV1 = {
|
|
1156
|
+
version: 1;
|
|
1157
|
+
normalize?: ('nbsp' | 'collapse_ws')[];
|
|
1158
|
+
/** Captions of the document (regex fragments, case-insensitive, followed by a colon). A labelled value ends at the next caption. */
|
|
1159
|
+
labels?: string[];
|
|
1160
|
+
/** Template of the canonical QR URL, rebuilt from the fields the QR carried (`{field}` placeholders). It survives readers that garble the URL. */
|
|
1161
|
+
qr_canonical?: string;
|
|
1162
|
+
fields: IdentityFieldRule[];
|
|
1163
|
+
};
|
|
834
1164
|
|
|
835
|
-
export type {
|
|
1165
|
+
export type { IdentityRulesV1 as $, Address as A, CustomerCapabilities as B, Cart as C, CustomerIdentifier as D, CustomerPasswordLoginInput as E, CustomerRegisterInput as F, CustomerRegisterResult as G, CustomerResetPasswordInput as H, CustomerSession as I, CustomerSessionInput as J, CustomerSetPasswordInput as K, CustomerUpdateInput as L, CustomerVerifyRegistrationInput as M, DeclineCode as N, DeliveryInput as O, DeliveryMethod as P, DeliveryOptions as Q, Facet as R, FacetValue as S, FiscalDocumentEvidence as T, FiscalDocumentSource as U, FiscalIdentity as V, FiscalIdentityInput as W, FiscalVerification as X, IdentityCapture as Y, IdentityDocumentSpec as Z, IdentityFieldRule as _, AddressInput as a, ShippingRate as a$, IdentityStrategy as a0, Image as a1, InsufficientStockDetails as a2, InsufficientStockLine as a3, InvoiceAction as a4, InvoiceRequest as a5, InvoiceResult as a6, InvoiceStage as a7, InvoiceSummary as a8, InvoicingCapabilities as a9, PaymentPoint as aA, PaymentSubmitInput as aB, PaymentVoucher as aC, PickupOption as aD, Product as aE, ProductAttributeValue as aF, ProductCategory as aG, ProductConnection as aH, ProductOption as aI, ProductSort as aJ, ProductVariant as aK, PromotionAnnouncement as aL, PromotionReward as aM, PromotionTier as aN, PromotionsList as aO, RecommendationPlacement as aP, Region as aQ, RegionFiscalEntry as aR, RegionFiscalId as aS, RegionPaymentMethod as aT, RestockSubscriptionInput as aU, RestockSubscriptionResult as aV, ReturnRequest as aW, ReturnRequestInput as aX, SEO as aY, Section as aZ, SelectedOption as a_, Location as aa, LocationContacts as ab, LocationKind as ac, LocationListParams as ad, LocationPickup as ae, Menu as af, MenuItem as ag, Money as ah, MoneyRange as ai, NearestLocation as aj, Order as ak, OrderLine as al, Page as am, PageInfo as an, PaymentAction as ao, PaymentAttemptResult as ap, PaymentAttemptStatus as aq, PaymentClient as ar, PaymentClientCsp as as, PaymentConfig as at, PaymentFlow as au, PaymentMethod as av, PaymentMethods as aw, PaymentNextStep as ax, PaymentOption as ay, PaymentOptionKey as az, Article as b, Shop as b0, ShopBranch as b1, ShopContact as b2, ShopPolicies as b3, StockLevel as b4, StorefrontErrorBody as b5, StorefrontErrorCode as b6, Theme as b7, TokenIntrospection as b8, UnavailableRateProvider as b9, Availability as c, AvailabilityStatus as d, CartBuyer as e, CartBuyerInput as f, CartCost as g, CartDelivery as h, CartDiscountCode as i, CartDiscountLine as j, CartInvoice as k, CartLine as l, CartLineInput as m, CartLineMerchandise as n, CartLineProblem as o, CartLineUpdateInput as p, CartPromotions as q, CartRecommendation as r, CartRecommendations as s, CheckoutInfo as t, CheckoutInput as u, CheckoutPayment as v, CheckoutResult as w, Collection as x, Connection as y, Customer as z };
|