@asteby/storefront 0.10.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 +149 -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 +93 -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 +91 -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 +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-DdLzlxw4.d.cts → types-C-Ley-Mt.d.cts} +293 -3
- package/dist/{types-COSwo4ND.d.ts → types-D7UJxrba.d.ts} +293 -3
- package/package.json +41 -3
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/fiscal-document/registry.ts
|
|
4
|
+
var extractors = /* @__PURE__ */ new Map();
|
|
5
|
+
function registerIdentityExtractor(key, fn) {
|
|
6
|
+
if (!key || typeof fn !== "function") throw new TypeError("registerIdentityExtractor: key and fn are required");
|
|
7
|
+
extractors.set(key, fn);
|
|
8
|
+
return () => {
|
|
9
|
+
if (extractors.get(key) === fn) extractors.delete(key);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function getIdentityExtractor(key) {
|
|
13
|
+
return extractors.get(key);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/fiscal-document/text.ts
|
|
17
|
+
var FOLD = {};
|
|
18
|
+
for (const [from, to] of [
|
|
19
|
+
["\xE1\xE0\xE2\xE3\xE4", "a"],
|
|
20
|
+
["\xE9\xE8\xEA\xEB", "e"],
|
|
21
|
+
["\xED\xEC\xEE\xEF", "i"],
|
|
22
|
+
["\xF3\xF2\xF4\xF5\xF6", "o"],
|
|
23
|
+
["\xFA\xF9\xFB\xFC", "u"],
|
|
24
|
+
["\xF1", "n"],
|
|
25
|
+
["\xE7", "c"]
|
|
26
|
+
]) for (const ch of from) FOLD[ch] = to;
|
|
27
|
+
function foldText(s) {
|
|
28
|
+
let out = "";
|
|
29
|
+
for (const ch of s.toLowerCase()) out += FOLD[ch] ?? ch;
|
|
30
|
+
return out.split(/\s+/).filter(Boolean).join(" ");
|
|
31
|
+
}
|
|
32
|
+
function ageInDays(isoDate, now) {
|
|
33
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
34
|
+
if (!m) return null;
|
|
35
|
+
const then = Date.UTC(+m[1], +m[2] - 1, +m[3]);
|
|
36
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
37
|
+
return Math.floor((today - then) / 864e5);
|
|
38
|
+
}
|
|
39
|
+
async function sha256Hex(data) {
|
|
40
|
+
const subtle = globalThis.crypto?.subtle;
|
|
41
|
+
if (!subtle) throw new TypeError("sha256Hex: Web Crypto (crypto.subtle) is not available");
|
|
42
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
43
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
44
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/fiscal-document/engine.ts
|
|
48
|
+
var MAX_TEXT_LENGTH = 4e5;
|
|
49
|
+
var MAX_FIELDS = 32;
|
|
50
|
+
var MAX_STRATEGIES = 8;
|
|
51
|
+
var MAX_PATTERN = 1024;
|
|
52
|
+
var STRATEGIES = /* @__PURE__ */ new Set(["regex", "labeled_lines", "concat_labeled", "catalog_phrases", "date", "qr_param"]);
|
|
53
|
+
var STRATEGY_KEYS = /* @__PURE__ */ new Set([
|
|
54
|
+
"strategy",
|
|
55
|
+
"pattern",
|
|
56
|
+
"flags",
|
|
57
|
+
"group",
|
|
58
|
+
"label",
|
|
59
|
+
"labels",
|
|
60
|
+
"separator",
|
|
61
|
+
"catalog",
|
|
62
|
+
"start",
|
|
63
|
+
"end",
|
|
64
|
+
"order",
|
|
65
|
+
"months",
|
|
66
|
+
"param",
|
|
67
|
+
"split",
|
|
68
|
+
"parts",
|
|
69
|
+
"match",
|
|
70
|
+
"upper",
|
|
71
|
+
"capture",
|
|
72
|
+
"scan"
|
|
73
|
+
]);
|
|
74
|
+
var RULES_KEYS = /* @__PURE__ */ new Set(["version", "normalize", "labels", "qr_canonical", "fields"]);
|
|
75
|
+
var PLACEHOLDER = /\{([a-z0-9_]+)\}/g;
|
|
76
|
+
var IdentityRulesError = class extends Error {
|
|
77
|
+
name = "IdentityRulesError";
|
|
78
|
+
};
|
|
79
|
+
var cache = /* @__PURE__ */ new Map();
|
|
80
|
+
function checkPattern(what, p, flags = "") {
|
|
81
|
+
if (!p) throw new IdentityRulesError(`rules: ${what}: empty pattern`);
|
|
82
|
+
if (p.length > MAX_PATTERN) throw new IdentityRulesError(`rules: ${what}: pattern longer than ${MAX_PATTERN}`);
|
|
83
|
+
for (const f of flags) if (f !== "i" && f !== "s" && f !== "m") throw new IdentityRulesError(`rules: ${what}: unknown flag "${f}"`);
|
|
84
|
+
if (p.replaceAll("(?:", "").includes("(?")) throw new IdentityRulesError(`rules: ${what}: only (?: groups are portable`);
|
|
85
|
+
for (const bad of ["[[:", "\\p", "\\P", "\\z", "\\A", "\\Q", "\\E", "\\C"]) {
|
|
86
|
+
if (p.includes(bad)) throw new IdentityRulesError(`rules: ${what}: ${bad} is not portable`);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
compiled(p, flags);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw err instanceof IdentityRulesError ? err : new IdentityRulesError(`rules: ${what}: invalid pattern`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function compiled(p, flags = "", global = false) {
|
|
95
|
+
const k = `${global ? "g" : ""}${flags}/${p}`;
|
|
96
|
+
let re = cache.get(k);
|
|
97
|
+
if (!re) {
|
|
98
|
+
re = new RegExp(p, flags + (global ? "g" : ""));
|
|
99
|
+
cache.set(k, re);
|
|
100
|
+
}
|
|
101
|
+
re.lastIndex = 0;
|
|
102
|
+
return re;
|
|
103
|
+
}
|
|
104
|
+
function validateStrategy(s, haveLabels) {
|
|
105
|
+
for (const k of Object.keys(s)) if (!STRATEGY_KEYS.has(k)) throw new IdentityRulesError(`rules: unknown member "${k}"`);
|
|
106
|
+
if (s.capture) {
|
|
107
|
+
checkPattern("capture", s.capture.pattern, s.capture.flags);
|
|
108
|
+
const groups = new RegExp(s.capture.pattern + "|").exec("").length - 1;
|
|
109
|
+
if ((s.capture.group ?? 0) < 0 || (s.capture.group ?? 0) > groups) throw new IdentityRulesError("rules: capture group does not exist");
|
|
110
|
+
}
|
|
111
|
+
switch (s.strategy) {
|
|
112
|
+
case "regex": {
|
|
113
|
+
checkPattern("pattern", s.pattern, s.flags);
|
|
114
|
+
const groups = new RegExp(s.pattern + "|").exec("").length - 1;
|
|
115
|
+
if ((s.group ?? 0) < 0 || (s.group ?? 0) > groups) throw new IdentityRulesError("rules: regex group does not exist");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case "labeled_lines":
|
|
119
|
+
if (!haveLabels) throw new IdentityRulesError("rules: labeled_lines needs the top-level labels list");
|
|
120
|
+
return checkPattern("label", s.label);
|
|
121
|
+
case "concat_labeled":
|
|
122
|
+
if (!haveLabels || !s.labels?.length) throw new IdentityRulesError("rules: concat_labeled needs labels");
|
|
123
|
+
return s.labels.forEach((l) => checkPattern("labels", l));
|
|
124
|
+
case "catalog_phrases":
|
|
125
|
+
if (!s.catalog) throw new IdentityRulesError("rules: catalog_phrases needs a catalog");
|
|
126
|
+
return [s.start, s.end].forEach((p) => p && checkPattern("section", p, "i"));
|
|
127
|
+
case "date": {
|
|
128
|
+
checkPattern("pattern", s.pattern, s.flags);
|
|
129
|
+
const order = s.order ?? "";
|
|
130
|
+
if (order.length !== 3 || ![..."dmy"].every((c) => order.split(c).length === 2)) throw new IdentityRulesError(`rules: date order "${order}" must be a permutation of d, m, y`);
|
|
131
|
+
if (new RegExp(s.pattern + "|").exec("").length - 1 < 3) throw new IdentityRulesError("rules: date pattern needs three groups");
|
|
132
|
+
if (s.months && s.months.length !== 0 && s.months.length !== 12) throw new IdentityRulesError("rules: date months wants 12 names");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
case "qr_param": {
|
|
136
|
+
if (!s.param && !s.scan) throw new IdentityRulesError("rules: qr_param needs a param or a scan");
|
|
137
|
+
if ((s.parts ?? 0) < 0) throw new IdentityRulesError("rules: qr_param parts must not be negative");
|
|
138
|
+
if (s.scan) {
|
|
139
|
+
checkPattern("scan", s.scan, s.flags);
|
|
140
|
+
const groups = new RegExp(s.scan + "|").exec("").length - 1;
|
|
141
|
+
if ((s.group ?? 0) < 1 || (s.group ?? 0) > groups) throw new IdentityRulesError("rules: scan group does not exist");
|
|
142
|
+
}
|
|
143
|
+
if (s.param) checkPattern("match", s.match);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
throw new IdentityRulesError(`rules: unknown strategy "${String(s.strategy)}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function validateRules(rules, discard = []) {
|
|
151
|
+
for (const k of Object.keys(rules ?? {})) if (!RULES_KEYS.has(k)) throw new IdentityRulesError(`rules: unknown member "${k}"`);
|
|
152
|
+
if (rules?.version !== 1) throw new IdentityRulesError(`rules: version ${String(rules?.version)} is not 1`);
|
|
153
|
+
for (const n of rules.normalize ?? []) if (n !== "nbsp" && n !== "collapse_ws") throw new IdentityRulesError(`rules: unknown normalize step "${String(n)}"`);
|
|
154
|
+
for (const l of rules.labels ?? []) checkPattern("labels", l);
|
|
155
|
+
if (!Array.isArray(rules.fields) || rules.fields.length === 0 || rules.fields.length > MAX_FIELDS) throw new IdentityRulesError(`rules: want 1..${MAX_FIELDS} fields`);
|
|
156
|
+
if (rules.qr_canonical) {
|
|
157
|
+
const keys = new Set(rules.fields.map((f) => f.key));
|
|
158
|
+
for (const m of rules.qr_canonical.matchAll(PLACEHOLDER)) if (!keys.has(m[1])) throw new IdentityRulesError(`rules: qr_canonical names unknown field "${m[1]}"`);
|
|
159
|
+
}
|
|
160
|
+
const drop = new Set(discard.map((d) => d.trim().toLowerCase()));
|
|
161
|
+
const seen = /* @__PURE__ */ new Set();
|
|
162
|
+
for (const f of rules.fields) {
|
|
163
|
+
if (!f.key || seen.has(f.key)) throw new IdentityRulesError(`rules: field key "${f.key}" is empty or repeated`);
|
|
164
|
+
seen.add(f.key);
|
|
165
|
+
if (drop.has(f.key.toLowerCase())) throw new IdentityRulesError(`rules: field "${f.key}" is in discard_fields`);
|
|
166
|
+
if (!Array.isArray(f.strategies) || f.strategies.length === 0 || f.strategies.length > MAX_STRATEGIES) throw new IdentityRulesError(`rules: field "${f.key}" wants 1..${MAX_STRATEGIES} strategies`);
|
|
167
|
+
for (const s of f.strategies) {
|
|
168
|
+
if (!STRATEGIES.has(s.strategy)) throw new IdentityRulesError(`rules: unknown strategy "${String(s.strategy)}"`);
|
|
169
|
+
validateStrategy(s, (rules.labels?.length ?? 0) > 0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function parseRules(spec) {
|
|
174
|
+
let rules = spec.rules;
|
|
175
|
+
if (typeof rules === "string") {
|
|
176
|
+
try {
|
|
177
|
+
rules = JSON.parse(rules);
|
|
178
|
+
} catch {
|
|
179
|
+
throw new IdentityRulesError(`${spec.kind}: rules is not valid JSON`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!rules || typeof rules !== "object") throw new IdentityRulesError(`${spec.kind}: rules@1 needs rules`);
|
|
183
|
+
validateRules(rules);
|
|
184
|
+
return rules;
|
|
185
|
+
}
|
|
186
|
+
function normalizeText(s, steps = []) {
|
|
187
|
+
let t = s.length > MAX_TEXT_LENGTH ? s.slice(0, MAX_TEXT_LENGTH) : s;
|
|
188
|
+
for (const st of steps) {
|
|
189
|
+
if (st === "nbsp") t = t.replaceAll("\xA0", " ");
|
|
190
|
+
else if (st === "collapse_ws") t = t.split(/\s+/).filter(Boolean).join(" ");
|
|
191
|
+
}
|
|
192
|
+
return t;
|
|
193
|
+
}
|
|
194
|
+
var LabelIndex = class {
|
|
195
|
+
constructor(labels, text) {
|
|
196
|
+
this.text = text;
|
|
197
|
+
if (labels.length === 0) return;
|
|
198
|
+
for (const m of text.matchAll(new RegExp(`(?:^|\\s)(${labels.join("|")})\\s*:`, "gi"))) {
|
|
199
|
+
this.hits.push({ caption: m[1] ?? "", start: m.index, from: m.index + m[0].length });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
text;
|
|
203
|
+
hits = [];
|
|
204
|
+
value(label) {
|
|
205
|
+
const full = compiled(`^(?:${label})$`, "i");
|
|
206
|
+
for (let i = 0; i < this.hits.length; i++) {
|
|
207
|
+
const h = this.hits[i];
|
|
208
|
+
if (!full.test(h.caption)) continue;
|
|
209
|
+
const end = i + 1 < this.hits.length ? this.hits[i + 1].start : this.text.length;
|
|
210
|
+
return this.text.slice(h.from, end).trim();
|
|
211
|
+
}
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function section(text, start, end) {
|
|
216
|
+
let from = 0;
|
|
217
|
+
if (start) {
|
|
218
|
+
const m = compiled(start, "is").exec(text);
|
|
219
|
+
if (!m) return "";
|
|
220
|
+
from = m.index + m[0].length;
|
|
221
|
+
}
|
|
222
|
+
let to = text.length;
|
|
223
|
+
if (end) {
|
|
224
|
+
const m = compiled(end, "is").exec(text.slice(from));
|
|
225
|
+
if (m) to = from + m.index;
|
|
226
|
+
}
|
|
227
|
+
return text.slice(from, to);
|
|
228
|
+
}
|
|
229
|
+
function phrasesOf(e) {
|
|
230
|
+
const src = e.match_phrases?.length ? e.match_phrases : [e.label, e.label_en ?? ""];
|
|
231
|
+
return [...new Set(src.map(foldText).filter(Boolean))];
|
|
232
|
+
}
|
|
233
|
+
function catalogHits(sec, entries = []) {
|
|
234
|
+
const hay = foldText(sec);
|
|
235
|
+
const hits = [];
|
|
236
|
+
for (const e of entries) {
|
|
237
|
+
let best = -1;
|
|
238
|
+
for (const p of phrasesOf(e)) {
|
|
239
|
+
const at = hay.indexOf(p);
|
|
240
|
+
if (at >= 0 && (best < 0 || at < best)) best = at;
|
|
241
|
+
}
|
|
242
|
+
if (best >= 0) hits.push({ code: e.code, at: best });
|
|
243
|
+
}
|
|
244
|
+
return hits.sort((a, b) => a.at - b.at).map((h) => h.code);
|
|
245
|
+
}
|
|
246
|
+
function parseDate(text, s) {
|
|
247
|
+
const m = compiled(s.pattern, s.flags ?? "").exec(text);
|
|
248
|
+
if (!m) return "";
|
|
249
|
+
let d = 0;
|
|
250
|
+
let mo = 0;
|
|
251
|
+
let y = 0;
|
|
252
|
+
for (let i = 0; i < 3; i++) {
|
|
253
|
+
const c = s.order[i];
|
|
254
|
+
const g = (m[i + 1] ?? "").trim();
|
|
255
|
+
let n = /^[+-]?\d+$/.test(g) ? Number(g) : NaN;
|
|
256
|
+
if (Number.isNaN(n) && c === "m" && s.months?.length === 12) {
|
|
257
|
+
n = 0;
|
|
258
|
+
s.months.forEach((name, j) => {
|
|
259
|
+
if (foldText(name) === foldText(g)) n = j + 1;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (Number.isNaN(n) || n === 0) return "";
|
|
263
|
+
if (c === "d") d = n;
|
|
264
|
+
else if (c === "m") mo = n;
|
|
265
|
+
else y = n;
|
|
266
|
+
}
|
|
267
|
+
const t = new Date(Date.UTC(y, mo - 1, d));
|
|
268
|
+
if (y < 1e3 || t.getUTCFullYear() !== y || t.getUTCMonth() + 1 !== mo || t.getUTCDate() !== d) return "";
|
|
269
|
+
return `${String(y).padStart(4, "0")}-${String(mo).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
270
|
+
}
|
|
271
|
+
function qrScan(qr, s) {
|
|
272
|
+
if (!s.scan) return "";
|
|
273
|
+
const m = compiled(s.scan, s.flags ?? "").exec(qr);
|
|
274
|
+
return m ? (m[s.group ?? 0] ?? "").trim().toUpperCase() : "";
|
|
275
|
+
}
|
|
276
|
+
function qrPart(qr, s) {
|
|
277
|
+
const url = qr.trim();
|
|
278
|
+
if (!url) return "";
|
|
279
|
+
if (!s.param) return qrScan(url, s);
|
|
280
|
+
const m = new RegExp(`[?&]${s.param.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^&\\s#]+)`, "i").exec(url);
|
|
281
|
+
if (!m) return qrScan(url, s);
|
|
282
|
+
let val;
|
|
283
|
+
try {
|
|
284
|
+
val = decodeURIComponent(m[1].replaceAll("+", " "));
|
|
285
|
+
} catch {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
const parts = s.split ? val.split(s.split) : [val];
|
|
289
|
+
if ((s.parts ?? 0) > 0 && parts.length !== s.parts) return qrScan(url, s);
|
|
290
|
+
const re = compiled(s.match);
|
|
291
|
+
for (const raw of parts) {
|
|
292
|
+
const p = raw.trim().toUpperCase();
|
|
293
|
+
if (re.test(p)) return p;
|
|
294
|
+
}
|
|
295
|
+
return qrScan(url, s);
|
|
296
|
+
}
|
|
297
|
+
function queryEscape(v) {
|
|
298
|
+
return encodeURIComponent(v).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()).replaceAll("%20", "+");
|
|
299
|
+
}
|
|
300
|
+
function post(s, value) {
|
|
301
|
+
let v = value.trim();
|
|
302
|
+
if (!v) return "";
|
|
303
|
+
if (s.upper) v = v.toUpperCase();
|
|
304
|
+
if (s.capture) {
|
|
305
|
+
const m = compiled(s.capture.pattern, s.capture.flags ?? "").exec(v);
|
|
306
|
+
if (!m) return "";
|
|
307
|
+
v = (m[s.capture.group ?? 0] ?? "").trim();
|
|
308
|
+
}
|
|
309
|
+
return v;
|
|
310
|
+
}
|
|
311
|
+
function run(s, text, labels, catalogs) {
|
|
312
|
+
switch (s.strategy) {
|
|
313
|
+
case "regex": {
|
|
314
|
+
const m = compiled(s.pattern, s.flags ?? "").exec(text);
|
|
315
|
+
return m ? [m[s.group ?? 0] ?? ""] : [];
|
|
316
|
+
}
|
|
317
|
+
case "labeled_lines":
|
|
318
|
+
return [labels.value(s.label)];
|
|
319
|
+
case "concat_labeled": {
|
|
320
|
+
const parts = (s.labels ?? []).map((l) => labels.value(l).trim()).filter(Boolean);
|
|
321
|
+
return [parts.join(s.separator || " ")];
|
|
322
|
+
}
|
|
323
|
+
case "catalog_phrases":
|
|
324
|
+
return catalogHits(section(text, s.start, s.end), catalogs[s.catalog]);
|
|
325
|
+
case "date":
|
|
326
|
+
return [parseDate(text, s)];
|
|
327
|
+
default:
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
var nonEmpty = (vals) => vals.map((v) => v.trim()).filter(Boolean);
|
|
332
|
+
function extractIdentityDocument(text, spec, options = {}) {
|
|
333
|
+
const discard = (spec.discard_fields ?? []).map((d) => d.trim().toLowerCase());
|
|
334
|
+
const out = {
|
|
335
|
+
kind: spec.kind,
|
|
336
|
+
extractor: spec.engine,
|
|
337
|
+
fields: {},
|
|
338
|
+
sources: {},
|
|
339
|
+
missing: [],
|
|
340
|
+
discarded: [...spec.discard_fields ?? []],
|
|
341
|
+
qr: { match: null, canonical: "" },
|
|
342
|
+
stale: null,
|
|
343
|
+
age_days: null
|
|
344
|
+
};
|
|
345
|
+
if (spec.engine === "rules@1") {
|
|
346
|
+
const rules = parseRules(spec);
|
|
347
|
+
const body = normalizeText(text, rules.normalize);
|
|
348
|
+
const labels = new LabelIndex(rules.labels ?? [], body);
|
|
349
|
+
const catalogs = options.catalogs ?? {};
|
|
350
|
+
let compared = 0;
|
|
351
|
+
let equal = 0;
|
|
352
|
+
const qrBest = {};
|
|
353
|
+
for (const f of rules.fields) {
|
|
354
|
+
let textVals = [];
|
|
355
|
+
const qrVals = [];
|
|
356
|
+
for (const s of f.strategies) {
|
|
357
|
+
if (s.strategy === "qr_param") {
|
|
358
|
+
const v = post(s, qrPart(options.qr ?? "", s));
|
|
359
|
+
if (v) qrVals.push(v);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (textVals.length > 0) continue;
|
|
363
|
+
textVals = nonEmpty(run(s, body, labels, catalogs).map((v) => post(s, v)));
|
|
364
|
+
}
|
|
365
|
+
let got = [];
|
|
366
|
+
if (textVals.length > 0) {
|
|
367
|
+
got = textVals;
|
|
368
|
+
out.sources[f.key] = "text";
|
|
369
|
+
} else if (qrVals.length > 0) {
|
|
370
|
+
got = qrVals.slice(0, 1);
|
|
371
|
+
out.sources[f.key] = "qr";
|
|
372
|
+
}
|
|
373
|
+
if (qrVals.length > 0) qrBest[f.key] = qrVals[0];
|
|
374
|
+
if (textVals.length > 0 && qrVals.length > 0) {
|
|
375
|
+
compared++;
|
|
376
|
+
if (textVals[0].toUpperCase() === qrVals[0].toUpperCase()) equal++;
|
|
377
|
+
}
|
|
378
|
+
if (got.length === 0) {
|
|
379
|
+
if (f.required) out.missing.push(f.key);
|
|
380
|
+
} else out.fields[f.key] = f.multiple ? got : got[0];
|
|
381
|
+
}
|
|
382
|
+
if (compared > 0) out.qr.match = compared === equal;
|
|
383
|
+
if (rules.qr_canonical && (options.qr ?? "").trim()) {
|
|
384
|
+
let ok = true;
|
|
385
|
+
const canon = rules.qr_canonical.replace(PLACEHOLDER, (_p, key) => {
|
|
386
|
+
const v = qrBest[key];
|
|
387
|
+
if (!v) ok = false;
|
|
388
|
+
return queryEscape(v ?? "");
|
|
389
|
+
});
|
|
390
|
+
if (ok) out.qr.canonical = canon;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
const id = spec.engine.startsWith("plugin:") ? spec.engine.slice("plugin:".length) : spec.engine;
|
|
394
|
+
const fn = getIdentityExtractor(id) ?? getIdentityExtractor(spec.kind);
|
|
395
|
+
if (!fn) throw new IdentityRulesError(`${spec.kind}: no extractor registered for engine "${spec.engine}"`);
|
|
396
|
+
const res = fn(text, spec, options);
|
|
397
|
+
out.fields = { ...res.fields };
|
|
398
|
+
out.sources = { ...res.sources ?? {} };
|
|
399
|
+
out.missing = [...res.missing ?? []];
|
|
400
|
+
out.qr.match = res.qr_match ?? null;
|
|
401
|
+
}
|
|
402
|
+
for (const k of Object.keys(out.fields)) {
|
|
403
|
+
if (discard.includes(k.toLowerCase())) {
|
|
404
|
+
delete out.fields[k];
|
|
405
|
+
delete out.sources[k];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const issued = out.fields.issued_on;
|
|
409
|
+
if (typeof issued === "string") {
|
|
410
|
+
out.age_days = ageInDays(issued, options.now ?? /* @__PURE__ */ new Date());
|
|
411
|
+
if (spec.freshness_days && out.age_days !== null) out.stale = out.age_days > spec.freshness_days;
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function catalogsFromRegion(fiscalId) {
|
|
416
|
+
return { fiscal_regime: fiscalId.regimes, invoice_use: fiscalId.invoice_uses };
|
|
417
|
+
}
|
|
418
|
+
function buildDocumentEvidence(extraction, input) {
|
|
419
|
+
const id = extraction.fields.document_id;
|
|
420
|
+
const issued = extraction.fields.issued_on;
|
|
421
|
+
return {
|
|
422
|
+
kind: extraction.kind,
|
|
423
|
+
document_id: typeof id === "string" ? id : null,
|
|
424
|
+
issued_on: typeof issued === "string" ? issued : null,
|
|
425
|
+
sha256: input.sha256 ?? null,
|
|
426
|
+
extractor: extraction.extractor,
|
|
427
|
+
source: input.source,
|
|
428
|
+
qr_match: extraction.qr.match
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/fiscal-document/errors.ts
|
|
433
|
+
var IdentityDocumentError = class extends Error {
|
|
434
|
+
name = "IdentityDocumentError";
|
|
435
|
+
code;
|
|
436
|
+
/** Finer cause: `bytes`, `pages`, `timeout`, `aborted`... */
|
|
437
|
+
reason;
|
|
438
|
+
constructor(code, message, options) {
|
|
439
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
440
|
+
this.code = code;
|
|
441
|
+
this.reason = options?.reason;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
function isIdentityDocumentError(err) {
|
|
445
|
+
return err instanceof IdentityDocumentError || typeof err === "object" && err !== null && err.name === "IdentityDocumentError";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/fiscal-document/pdf.ts
|
|
449
|
+
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
450
|
+
var DEFAULT_MAX_PAGES = 6;
|
|
451
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
452
|
+
var custom;
|
|
453
|
+
function setPdfjsLoader(loader) {
|
|
454
|
+
custom = loader;
|
|
455
|
+
}
|
|
456
|
+
async function loadPdfjs(workerSrc) {
|
|
457
|
+
const mod = custom ? await custom() : await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
458
|
+
if (workerSrc && mod.GlobalWorkerOptions) mod.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
459
|
+
const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
460
|
+
if (inBrowser && !mod.GlobalWorkerOptions?.workerSrc) {
|
|
461
|
+
throw new TypeError("readPdfText: set `workerSrc` to a pdf.worker.mjs your site serves (there is no default and no CDN)");
|
|
462
|
+
}
|
|
463
|
+
return mod;
|
|
464
|
+
}
|
|
465
|
+
async function toBytes(source) {
|
|
466
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
|
|
467
|
+
if (source instanceof ArrayBuffer) return new Uint8Array(source);
|
|
468
|
+
if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
469
|
+
throw new TypeError("readPdfText: expected a File, Blob, ArrayBuffer or Uint8Array");
|
|
470
|
+
}
|
|
471
|
+
function sizeOf(source) {
|
|
472
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) return source.size;
|
|
473
|
+
return source.byteLength;
|
|
474
|
+
}
|
|
475
|
+
function hasPdfSignature(bytes) {
|
|
476
|
+
return bytes.length >= 5 && bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70 && bytes[4] === 45;
|
|
477
|
+
}
|
|
478
|
+
function mapPdfjsError(err) {
|
|
479
|
+
const name = err?.name ?? "";
|
|
480
|
+
if (name === "PasswordException") return new IdentityDocumentError("password", "The PDF is password protected", { cause: err });
|
|
481
|
+
if (name === "InvalidPDFException") return new IdentityDocumentError("unreadable", "The PDF is corrupt or unsupported", { cause: err, reason: "invalid" });
|
|
482
|
+
return new IdentityDocumentError("unreadable", "The PDF could not be read", { cause: err });
|
|
483
|
+
}
|
|
484
|
+
async function openPdf(source, options = {}) {
|
|
485
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
486
|
+
const maxPages = options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
487
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
488
|
+
if (sizeOf(source) > maxBytes) throw new IdentityDocumentError("too_large", `The file is larger than ${maxBytes} bytes`, { reason: "bytes" });
|
|
489
|
+
const bytes = await toBytes(source);
|
|
490
|
+
if (bytes.byteLength > maxBytes) throw new IdentityDocumentError("too_large", `The file is larger than ${maxBytes} bytes`, { reason: "bytes" });
|
|
491
|
+
if (!hasPdfSignature(bytes)) throw new IdentityDocumentError("not_pdf", "The file is not a PDF");
|
|
492
|
+
const sha256 = await sha256Hex(bytes);
|
|
493
|
+
if (options.signal?.aborted) throw new IdentityDocumentError("unreadable", "Aborted", { reason: "aborted" });
|
|
494
|
+
const pdfjs = await loadPdfjs(options.workerSrc);
|
|
495
|
+
const task = pdfjs.getDocument({
|
|
496
|
+
data: bytes.slice(),
|
|
497
|
+
isEvalSupported: false,
|
|
498
|
+
enableXfa: false,
|
|
499
|
+
useSystemFonts: false,
|
|
500
|
+
disableAutoFetch: true,
|
|
501
|
+
disableStream: true,
|
|
502
|
+
useWorkerFetch: false,
|
|
503
|
+
verbosity: 0
|
|
504
|
+
});
|
|
505
|
+
task.promise.catch(() => void 0);
|
|
506
|
+
let doc;
|
|
507
|
+
const destroy = async () => {
|
|
508
|
+
try {
|
|
509
|
+
await (doc?.destroy ? doc.destroy() : task.destroy());
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
let timer;
|
|
514
|
+
let onAbort;
|
|
515
|
+
const limit = new Promise((_, reject) => {
|
|
516
|
+
timer = setTimeout(() => reject(new IdentityDocumentError("unreadable", `Reading took longer than ${timeoutMs} ms`, { reason: "timeout" })), timeoutMs);
|
|
517
|
+
if (options.signal) {
|
|
518
|
+
onAbort = () => reject(new IdentityDocumentError("unreadable", "Aborted", { reason: "aborted" }));
|
|
519
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
const cleanup = () => {
|
|
523
|
+
if (timer) clearTimeout(timer);
|
|
524
|
+
if (onAbort) options.signal?.removeEventListener("abort", onAbort);
|
|
525
|
+
};
|
|
526
|
+
try {
|
|
527
|
+
doc = await Promise.race([task.promise, limit]);
|
|
528
|
+
if (doc.numPages > maxPages) throw new IdentityDocumentError("too_large", `The PDF has more than ${maxPages} pages`, { reason: "pages" });
|
|
529
|
+
cleanup();
|
|
530
|
+
return { bytes, sha256, pdfjs, doc, destroy };
|
|
531
|
+
} catch (err) {
|
|
532
|
+
cleanup();
|
|
533
|
+
await destroy();
|
|
534
|
+
throw err instanceof IdentityDocumentError ? err : mapPdfjsError(err);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function itemsToLines(items) {
|
|
538
|
+
const rows = [];
|
|
539
|
+
for (const it of items) {
|
|
540
|
+
const s = it.str;
|
|
541
|
+
if (!s || !s.trim()) continue;
|
|
542
|
+
const x = it.transform?.[4] ?? 0;
|
|
543
|
+
const y = it.transform?.[5] ?? 0;
|
|
544
|
+
const row = rows.find((r) => Math.abs(r.y - y) <= 2);
|
|
545
|
+
if (row) row.parts.push({ x, s });
|
|
546
|
+
else rows.push({ y, parts: [{ x, s }] });
|
|
547
|
+
}
|
|
548
|
+
rows.sort((a, b) => b.y - a.y);
|
|
549
|
+
return rows.map((r) => r.parts.sort((a, b) => a.x - b.x).map((p) => p.s.trim()).join(" ")).join("\n");
|
|
550
|
+
}
|
|
551
|
+
async function readPdfText(source, options = {}) {
|
|
552
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
553
|
+
const opened = await openPdf(source, options);
|
|
554
|
+
let timer;
|
|
555
|
+
try {
|
|
556
|
+
const read = (async () => {
|
|
557
|
+
const pages2 = [];
|
|
558
|
+
for (let n = 1; n <= opened.doc.numPages; n++) {
|
|
559
|
+
const page = await opened.doc.getPage(n);
|
|
560
|
+
pages2.push(itemsToLines((await page.getTextContent()).items));
|
|
561
|
+
page.cleanup?.();
|
|
562
|
+
}
|
|
563
|
+
return pages2;
|
|
564
|
+
})();
|
|
565
|
+
const limit = new Promise((_, reject) => {
|
|
566
|
+
timer = setTimeout(() => reject(new IdentityDocumentError("unreadable", `Reading took longer than ${timeoutMs} ms`, { reason: "timeout" })), timeoutMs);
|
|
567
|
+
});
|
|
568
|
+
const pages = await Promise.race([read, limit]);
|
|
569
|
+
const text = pages.join("\n\n");
|
|
570
|
+
if (text.replace(/\s+/g, "").length < (options.minChars ?? 20)) throw new IdentityDocumentError("no_text", "The PDF has no text layer");
|
|
571
|
+
return { text, pages: pages.length, sha256: opened.sha256 };
|
|
572
|
+
} catch (err) {
|
|
573
|
+
throw err instanceof IdentityDocumentError ? err : mapPdfjsError(err);
|
|
574
|
+
} finally {
|
|
575
|
+
if (timer) clearTimeout(timer);
|
|
576
|
+
await opened.destroy();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/fiscal-document/qr.ts
|
|
581
|
+
function makeCanvas(width, height) {
|
|
582
|
+
const g = globalThis;
|
|
583
|
+
if (g.OffscreenCanvas) return new g.OffscreenCanvas(width, height);
|
|
584
|
+
if (g.document) {
|
|
585
|
+
const c = g.document.createElement("canvas");
|
|
586
|
+
c.width = width;
|
|
587
|
+
c.height = height;
|
|
588
|
+
return c;
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
async function decodeImage(image, canvas, custom2) {
|
|
593
|
+
if (custom2) return await custom2(image) ?? null;
|
|
594
|
+
const Detector = globalThis.BarcodeDetector;
|
|
595
|
+
if (Detector) {
|
|
596
|
+
try {
|
|
597
|
+
const found = await new Detector({ formats: ["qr_code"] }).detect(canvas);
|
|
598
|
+
if (found[0]?.rawValue) return found[0].rawValue;
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const mod = await import('jsqr');
|
|
604
|
+
return mod.default(image.data, image.width, image.height)?.data ?? null;
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function scan(page, scale, custom2) {
|
|
610
|
+
const viewport = page.getViewport({ scale });
|
|
611
|
+
const canvas = makeCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
|
|
612
|
+
if (!canvas && !custom2) return null;
|
|
613
|
+
const c = canvas ?? { width: Math.ceil(viewport.width), height: Math.ceil(viewport.height), getContext: () => null };
|
|
614
|
+
const context = c.getContext("2d");
|
|
615
|
+
if (!context) return null;
|
|
616
|
+
await page.render({ canvasContext: context, viewport }).promise;
|
|
617
|
+
return decodeImage(context.getImageData(0, 0, c.width, c.height), c, custom2);
|
|
618
|
+
}
|
|
619
|
+
async function decodePdfQr(source, options = {}) {
|
|
620
|
+
const opened = await openPdf(source, options);
|
|
621
|
+
try {
|
|
622
|
+
const last = opened.doc.numPages;
|
|
623
|
+
const wanted = options.pages ?? [1, last];
|
|
624
|
+
const pages = [...new Set(wanted)].filter((n) => n >= 1 && n <= last);
|
|
625
|
+
for (const n of pages) {
|
|
626
|
+
try {
|
|
627
|
+
const page = await opened.doc.getPage(n);
|
|
628
|
+
const found = await scan(page, options.scale ?? 2, options.decode);
|
|
629
|
+
page.cleanup?.();
|
|
630
|
+
if (found) return found;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (err instanceof IdentityDocumentError) throw err;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
} finally {
|
|
637
|
+
await opened.destroy();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
exports.DEFAULT_MAX_BYTES = DEFAULT_MAX_BYTES;
|
|
642
|
+
exports.DEFAULT_MAX_PAGES = DEFAULT_MAX_PAGES;
|
|
643
|
+
exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
|
|
644
|
+
exports.IdentityDocumentError = IdentityDocumentError;
|
|
645
|
+
exports.IdentityRulesError = IdentityRulesError;
|
|
646
|
+
exports.MAX_TEXT_LENGTH = MAX_TEXT_LENGTH;
|
|
647
|
+
exports.buildDocumentEvidence = buildDocumentEvidence;
|
|
648
|
+
exports.catalogsFromRegion = catalogsFromRegion;
|
|
649
|
+
exports.decodePdfQr = decodePdfQr;
|
|
650
|
+
exports.extractIdentityDocument = extractIdentityDocument;
|
|
651
|
+
exports.foldText = foldText;
|
|
652
|
+
exports.isIdentityDocumentError = isIdentityDocumentError;
|
|
653
|
+
exports.readPdfText = readPdfText;
|
|
654
|
+
exports.registerIdentityExtractor = registerIdentityExtractor;
|
|
655
|
+
exports.setPdfjsLoader = setPdfjsLoader;
|
|
656
|
+
exports.sha256Hex = sha256Hex;
|
|
657
|
+
exports.validateRules = validateRules;
|
|
658
|
+
//# sourceMappingURL=fiscal-document.cjs.map
|
|
659
|
+
//# sourceMappingURL=fiscal-document.cjs.map
|