@doccloak/core 0.8.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/CHANGELOG.md +35 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +230 -0
- package/dist/doc.d.ts +13 -0
- package/dist/doc.js +683 -0
- package/dist/docx.d.ts +31 -0
- package/dist/docx.js +26 -0
- package/dist/dom/docx.d.ts +90 -0
- package/dist/dom/docx.js +472 -0
- package/dist/dom/index.d.ts +11 -0
- package/dist/dom/index.js +11 -0
- package/dist/dom/ocr.d.ts +112 -0
- package/dist/dom/ocr.js +224 -0
- package/dist/engine.d.ts +81 -0
- package/dist/engine.js +274 -0
- package/dist/env.d.ts +49 -0
- package/dist/env.js +53 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +29 -0
- package/dist/model-loader.d.ts +55 -0
- package/dist/model-loader.js +253 -0
- package/dist/pipeline.d.ts +19 -0
- package/dist/pipeline.js +128 -0
- package/dist/providers/bardsai.d.ts +38 -0
- package/dist/providers/bardsai.js +393 -0
- package/dist/providers/gliner.d.ts +52 -0
- package/dist/providers/gliner.js +360 -0
- package/dist/regex/index.d.ts +17 -0
- package/dist/regex/index.js +18 -0
- package/dist/regex/loader.d.ts +44 -0
- package/dist/regex/loader.js +44 -0
- package/dist/regex/rules.data.d.ts +2 -0
- package/dist/regex/rules.data.js +2671 -0
- package/dist/regex/types.d.ts +27 -0
- package/dist/regex/types.js +1 -0
- package/dist/regex/validators.d.ts +17 -0
- package/dist/regex/validators.js +492 -0
- package/dist/session.d.ts +59 -0
- package/dist/session.js +153 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +28 -0
- package/dist/worker-protocol.d.ts +119 -0
- package/dist/worker-protocol.js +285 -0
- package/package.json +75 -0
- package/rules/at.json +70 -0
- package/rules/be.json +70 -0
- package/rules/ch.json +66 -0
- package/rules/cn.json +155 -0
- package/rules/de.json +192 -0
- package/rules/dk.json +43 -0
- package/rules/es.json +158 -0
- package/rules/fi.json +43 -0
- package/rules/fr.json +178 -0
- package/rules/gb.json +194 -0
- package/rules/ie.json +48 -0
- package/rules/it.json +116 -0
- package/rules/jp.json +136 -0
- package/rules/nl.json +99 -0
- package/rules/no.json +132 -0
- package/rules/pl.json +210 -0
- package/rules/pt.json +113 -0
- package/rules/schema.json +91 -0
- package/rules/se.json +149 -0
- package/rules/universal.json +279 -0
- package/rules/us.json +218 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLiNER PII Edge provider.
|
|
3
|
+
*
|
|
4
|
+
* Environment-agnostic: wasm paths, custom-label persistence (KV), model
|
|
5
|
+
* bytes (blob cache + fetch) and the tokenizer all arrive through the
|
|
6
|
+
* injected CoreEnv. This module never touches import.meta, localStorage,
|
|
7
|
+
* caches, navigator or @huggingface/transformers directly.
|
|
8
|
+
*/
|
|
9
|
+
import * as ort from 'onnxruntime-web';
|
|
10
|
+
import { fetchModelBlob, retryAsync } from "../model-loader.js";
|
|
11
|
+
// ── Model config ──────────────────────────────────────────
|
|
12
|
+
const DEFAULT_MODEL_URL = 'https://huggingface.co/knowledgator/gliner-pii-edge-v1.0/resolve/main/onnx/model_quint8.onnx';
|
|
13
|
+
const DEFAULT_TOKENIZER_HF = 'knowledgator/gliner-pii-edge-v1.0';
|
|
14
|
+
const DEFAULT_MODEL_NAME = 'GLiNER PII Edge';
|
|
15
|
+
const CUSTOM_LABELS_STORAGE_KEY = 'doccloak-custom-labels';
|
|
16
|
+
const CLS_TOKEN_ID = 50281;
|
|
17
|
+
const SEP_TOKEN_ID = 50282;
|
|
18
|
+
const ENT_TOKEN_ID = 50368;
|
|
19
|
+
const SEP_PROMPT_TOKEN_ID = 50369; // <<SEP>> (prompt separator, not [SEP])
|
|
20
|
+
const DEFAULT_THRESHOLD = 0.35;
|
|
21
|
+
const MAX_WORDS_PER_CHUNK = 150;
|
|
22
|
+
const CHUNK_OVERLAP = 40;
|
|
23
|
+
const DEFAULT_PII_LABELS = [
|
|
24
|
+
'person name',
|
|
25
|
+
'calendar date',
|
|
26
|
+
'email address', 'phone number', 'ip address',
|
|
27
|
+
'street address', 'city', 'zip code',
|
|
28
|
+
'bank account number', 'credit card number', 'iban',
|
|
29
|
+
'social security number', 'tax id', 'national id number',
|
|
30
|
+
'passport number', 'driver license number',
|
|
31
|
+
'money amount',
|
|
32
|
+
'company name',
|
|
33
|
+
];
|
|
34
|
+
// ── Label → EntityType mapping ────────────────────────────
|
|
35
|
+
function mapLabelToEntityType(label) {
|
|
36
|
+
const l = label.toLowerCase();
|
|
37
|
+
if (l === 'person name')
|
|
38
|
+
return 'PERSON';
|
|
39
|
+
if (l === 'email address')
|
|
40
|
+
return 'EMAIL';
|
|
41
|
+
if (l === 'phone number')
|
|
42
|
+
return 'PHONE';
|
|
43
|
+
if (['social security number', 'national id number', 'tax id', 'passport number', 'driver license number'].includes(l))
|
|
44
|
+
return 'SSN';
|
|
45
|
+
if (l === 'credit card number')
|
|
46
|
+
return 'CREDIT_CARD';
|
|
47
|
+
if (l === 'calendar date')
|
|
48
|
+
return 'DATE';
|
|
49
|
+
if (l === 'money amount')
|
|
50
|
+
return 'CURRENCY';
|
|
51
|
+
if (l === 'ip address')
|
|
52
|
+
return 'IP_ADDRESS';
|
|
53
|
+
if (['iban', 'bank account number'].includes(l))
|
|
54
|
+
return 'IBAN';
|
|
55
|
+
if (['street address', 'city', 'zip code'].includes(l))
|
|
56
|
+
return 'ADDRESS';
|
|
57
|
+
if (l === 'company name')
|
|
58
|
+
return 'COMPANY';
|
|
59
|
+
return 'OTHER';
|
|
60
|
+
}
|
|
61
|
+
// ── Word splitter ─────────────────────────────────────────
|
|
62
|
+
const WORD_PATTERN = /[\p{L}\p{N}]+(?:[-_][\p{L}\p{N}]+)*|\S/gu;
|
|
63
|
+
function splitWords(text) {
|
|
64
|
+
const words = [];
|
|
65
|
+
const starts = [];
|
|
66
|
+
const ends = [];
|
|
67
|
+
let match;
|
|
68
|
+
WORD_PATTERN.lastIndex = 0;
|
|
69
|
+
while ((match = WORD_PATTERN.exec(text)) !== null) {
|
|
70
|
+
words.push(match[0]);
|
|
71
|
+
starts.push(match.index);
|
|
72
|
+
ends.push(WORD_PATTERN.lastIndex);
|
|
73
|
+
}
|
|
74
|
+
return { words, starts, ends };
|
|
75
|
+
}
|
|
76
|
+
// ── Sigmoid ───────────────────────────────────────────────
|
|
77
|
+
function sigmoid(x) {
|
|
78
|
+
return 1 / (1 + Math.exp(-x));
|
|
79
|
+
}
|
|
80
|
+
function greedySelect(spans) {
|
|
81
|
+
const sorted = spans.slice().sort((a, b) => b[4] - a[4]);
|
|
82
|
+
const selected = [];
|
|
83
|
+
for (const span of sorted) {
|
|
84
|
+
const overlaps = selected.some(s => span[1] < s[2] && span[2] > s[1]);
|
|
85
|
+
if (!overlaps)
|
|
86
|
+
selected.push(span);
|
|
87
|
+
}
|
|
88
|
+
return selected.sort((a, b) => a[1] - b[1]);
|
|
89
|
+
}
|
|
90
|
+
// ── Provider ──────────────────────────────────────────────
|
|
91
|
+
export class GlinerProvider {
|
|
92
|
+
get name() { return this._name; }
|
|
93
|
+
constructor(env) {
|
|
94
|
+
this._name = DEFAULT_MODEL_NAME;
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
this.tokenizer = null;
|
|
97
|
+
this.session = null;
|
|
98
|
+
this.loading = false;
|
|
99
|
+
this.loadError = null;
|
|
100
|
+
this.loadWaiters = [];
|
|
101
|
+
this.progressCallback = null;
|
|
102
|
+
this.threshold = DEFAULT_THRESHOLD;
|
|
103
|
+
this.customLabels = [];
|
|
104
|
+
this.env = env;
|
|
105
|
+
}
|
|
106
|
+
isLoaded() {
|
|
107
|
+
return this.session !== null && this.tokenizer !== null;
|
|
108
|
+
}
|
|
109
|
+
isLoading() {
|
|
110
|
+
return this.loading;
|
|
111
|
+
}
|
|
112
|
+
onProgress(callback) {
|
|
113
|
+
this.progressCallback = callback;
|
|
114
|
+
}
|
|
115
|
+
setThreshold(value) {
|
|
116
|
+
this.threshold = Math.max(0.05, Math.min(0.95, value));
|
|
117
|
+
}
|
|
118
|
+
getThreshold() {
|
|
119
|
+
return this.threshold;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the combined list of all active labels (built-in + custom).
|
|
123
|
+
*/
|
|
124
|
+
getActiveLabels() {
|
|
125
|
+
return [...DEFAULT_PII_LABELS, ...this.customLabels];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get user-defined custom labels.
|
|
129
|
+
*/
|
|
130
|
+
getCustomLabels() {
|
|
131
|
+
return [...this.customLabels];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Set custom labels and persist through the injected KV store. The
|
|
135
|
+
* in-memory list updates synchronously (before the first await), so
|
|
136
|
+
* fire-and-forget callers keep the previous behavior.
|
|
137
|
+
*/
|
|
138
|
+
async setCustomLabels(labels) {
|
|
139
|
+
this.customLabels = labels.filter((l) => l.trim().length > 0);
|
|
140
|
+
try {
|
|
141
|
+
await this.env.kv.set(CUSTOM_LABELS_STORAGE_KEY, JSON.stringify(this.customLabels));
|
|
142
|
+
}
|
|
143
|
+
catch { /* KV backend unavailable (e.g. storage-less Web Worker) */ }
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Restore custom labels from the injected KV store on startup.
|
|
147
|
+
*/
|
|
148
|
+
async restoreCustomLabels() {
|
|
149
|
+
try {
|
|
150
|
+
const saved = await this.env.kv.get(CUSTOM_LABELS_STORAGE_KEY);
|
|
151
|
+
if (saved) {
|
|
152
|
+
this.customLabels = JSON.parse(saved);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* KV backend unavailable in Web Worker - labels are passed via message */ }
|
|
156
|
+
}
|
|
157
|
+
async load() {
|
|
158
|
+
if (this.isLoaded())
|
|
159
|
+
return;
|
|
160
|
+
if (this.loadError)
|
|
161
|
+
throw this.loadError;
|
|
162
|
+
if (this.loading) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
this.loadWaiters.push({ resolve, reject });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
this.loading = true;
|
|
168
|
+
try {
|
|
169
|
+
ort.env.wasm.wasmPaths = this.env.wasm.paths;
|
|
170
|
+
// Default to single-threaded WASM. ORT's multi-threaded path hangs
|
|
171
|
+
// silently in some cross-origin-isolated browser contexts when spawning
|
|
172
|
+
// pthread workers (model downloads but session.create never resolves).
|
|
173
|
+
ort.env.wasm.numThreads = this.env.wasm.numThreads ?? 1;
|
|
174
|
+
const loaderEnv = {
|
|
175
|
+
cache: this.env.modelCache,
|
|
176
|
+
fetch: this.env.fetch,
|
|
177
|
+
persistStorage: this.env.persistStorage,
|
|
178
|
+
};
|
|
179
|
+
// Load tokenizer and model in parallel (skip tokenizer if already loaded from a prior session)
|
|
180
|
+
const tasks = [
|
|
181
|
+
fetchModelBlob(loaderEnv, DEFAULT_MODEL_URL, (downloaded, total) => this.progressCallback?.(downloaded, total)),
|
|
182
|
+
];
|
|
183
|
+
if (!this.tokenizer) {
|
|
184
|
+
tasks.push(retryAsync(() => this.env.loadTokenizer(DEFAULT_TOKENIZER_HF), 'Tokenizer download').then((t) => {
|
|
185
|
+
this.tokenizer = t;
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
const [modelBlob] = await Promise.all(tasks);
|
|
189
|
+
const blobUrl = URL.createObjectURL(modelBlob);
|
|
190
|
+
try {
|
|
191
|
+
this.session = await ort.InferenceSession.create(blobUrl, {
|
|
192
|
+
executionProviders: ['wasm'],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
// ONNX Runtime has already read the data into the WASM heap
|
|
197
|
+
URL.revokeObjectURL(blobUrl);
|
|
198
|
+
}
|
|
199
|
+
console.info(`[DocCloak] Model loaded: ${this._name} | inputs: [${this.session.inputNames.join(', ')}] | outputs: [${this.session.outputNames.join(', ')}]`);
|
|
200
|
+
this.loading = false;
|
|
201
|
+
this.loadWaiters.forEach((w) => w.resolve());
|
|
202
|
+
this.loadWaiters.length = 0;
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
this.session = null;
|
|
206
|
+
this.loadError = err instanceof Error ? err : new Error(String(err));
|
|
207
|
+
this.loading = false;
|
|
208
|
+
this.loadWaiters.forEach((w) => w.reject(this.loadError));
|
|
209
|
+
this.loadWaiters.length = 0;
|
|
210
|
+
throw this.loadError;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
release() {
|
|
214
|
+
if (this.session) {
|
|
215
|
+
this.session.release();
|
|
216
|
+
this.session = null;
|
|
217
|
+
}
|
|
218
|
+
this.loading = false;
|
|
219
|
+
this.loadError = null;
|
|
220
|
+
}
|
|
221
|
+
async detect(text, onProgress) {
|
|
222
|
+
if (!this.isLoaded())
|
|
223
|
+
await this.load();
|
|
224
|
+
if (!this.session || !this.tokenizer)
|
|
225
|
+
return [];
|
|
226
|
+
const { words, starts, ends } = splitWords(text);
|
|
227
|
+
if (words.length === 0)
|
|
228
|
+
return [];
|
|
229
|
+
// Build prompt tokens once (shared across all chunks)
|
|
230
|
+
const activeLabels = this.getActiveLabels();
|
|
231
|
+
const promptTokens = this.buildPromptTokens(activeLabels);
|
|
232
|
+
const chunkSize = MAX_WORDS_PER_CHUNK;
|
|
233
|
+
const overlap = CHUNK_OVERLAP;
|
|
234
|
+
// Split into chunks if text is too long
|
|
235
|
+
const allSpans = [];
|
|
236
|
+
onProgress?.(0);
|
|
237
|
+
if (words.length <= chunkSize) {
|
|
238
|
+
// Single chunk — no splitting needed
|
|
239
|
+
const spans = await this.inferChunk(words, starts, ends, text, promptTokens, activeLabels);
|
|
240
|
+
allSpans.push(...spans);
|
|
241
|
+
onProgress?.(1);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Count total chunks
|
|
245
|
+
let totalChunks = 0;
|
|
246
|
+
for (let cs = 0; cs < words.length; cs += chunkSize - overlap) {
|
|
247
|
+
totalChunks++;
|
|
248
|
+
if (Math.min(cs + chunkSize, words.length) >= words.length)
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
// Multiple overlapping chunks
|
|
252
|
+
let chunksDone = 0;
|
|
253
|
+
for (let chunkStart = 0; chunkStart < words.length; chunkStart += chunkSize - overlap) {
|
|
254
|
+
const chunkEnd = Math.min(chunkStart + chunkSize, words.length);
|
|
255
|
+
const chunkWords = words.slice(chunkStart, chunkEnd);
|
|
256
|
+
const chunkStarts = starts.slice(chunkStart, chunkEnd);
|
|
257
|
+
const chunkEnds = ends.slice(chunkStart, chunkEnd);
|
|
258
|
+
const spans = await this.inferChunk(chunkWords, chunkStarts, chunkEnds, text, promptTokens, activeLabels);
|
|
259
|
+
allSpans.push(...spans);
|
|
260
|
+
chunksDone++;
|
|
261
|
+
onProgress?.(chunksDone / totalChunks);
|
|
262
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
263
|
+
if (chunkEnd >= words.length)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Greedy non-overlapping selection across all chunks
|
|
268
|
+
const selected = greedySelect(allSpans);
|
|
269
|
+
return selected
|
|
270
|
+
.filter(([spanText]) => spanText.trim().length >= 2)
|
|
271
|
+
.map(([spanText, start, end, label, score]) => ({
|
|
272
|
+
type: mapLabelToEntityType(label),
|
|
273
|
+
value: spanText,
|
|
274
|
+
start,
|
|
275
|
+
end,
|
|
276
|
+
confidence: score,
|
|
277
|
+
detector: `gliner:${label}`,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
buildPromptTokens(labels) {
|
|
281
|
+
const tokens = [];
|
|
282
|
+
for (const label of labels) {
|
|
283
|
+
tokens.push(ENT_TOKEN_ID);
|
|
284
|
+
for (const part of label.split(' ')) {
|
|
285
|
+
const subTokens = this.tokenizer.encode(part).slice(1, -1);
|
|
286
|
+
tokens.push(...subTokens);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
tokens.push(SEP_PROMPT_TOKEN_ID);
|
|
290
|
+
return tokens;
|
|
291
|
+
}
|
|
292
|
+
async inferChunk(words, charStarts, charEnds, fullText, promptTokens, labels) {
|
|
293
|
+
if (!this.session)
|
|
294
|
+
return [];
|
|
295
|
+
// Build token sequence: [CLS] + prompt + text words + [SEP]
|
|
296
|
+
const inputIds = [CLS_TOKEN_ID, ...promptTokens];
|
|
297
|
+
const attentionMask = new Array(inputIds.length).fill(1);
|
|
298
|
+
const wordsMask = new Array(inputIds.length).fill(0);
|
|
299
|
+
let wordCounter = 1;
|
|
300
|
+
for (const word of words) {
|
|
301
|
+
const subTokens = this.tokenizer.encode(word).slice(1, -1);
|
|
302
|
+
for (let j = 0; j < subTokens.length; j++) {
|
|
303
|
+
inputIds.push(subTokens[j]);
|
|
304
|
+
attentionMask.push(1);
|
|
305
|
+
wordsMask.push(j === 0 ? wordCounter : 0);
|
|
306
|
+
}
|
|
307
|
+
wordCounter++;
|
|
308
|
+
}
|
|
309
|
+
inputIds.push(SEP_TOKEN_ID);
|
|
310
|
+
attentionMask.push(1);
|
|
311
|
+
wordsMask.push(0);
|
|
312
|
+
const seqLen = inputIds.length;
|
|
313
|
+
const textLength = words.length;
|
|
314
|
+
const numEntities = labels.length;
|
|
315
|
+
const idToClass = {};
|
|
316
|
+
labels.forEach((label, i) => { idToClass[i + 1] = label; });
|
|
317
|
+
const feeds = {
|
|
318
|
+
input_ids: new ort.Tensor('int64', BigInt64Array.from(inputIds.map(BigInt)), [1, seqLen]),
|
|
319
|
+
attention_mask: new ort.Tensor('int64', BigInt64Array.from(attentionMask.map(BigInt)), [1, seqLen]),
|
|
320
|
+
words_mask: new ort.Tensor('int64', BigInt64Array.from(wordsMask.map(BigInt)), [1, seqLen]),
|
|
321
|
+
text_lengths: new ort.Tensor('int64', BigInt64Array.from([BigInt(textLength)]), [1, 1]),
|
|
322
|
+
};
|
|
323
|
+
const results = await this.session.run(feeds);
|
|
324
|
+
const outputName = this.session.outputNames[0] || 'logits';
|
|
325
|
+
const logits = results[outputName].data;
|
|
326
|
+
// Token-based: logits shape [1, num_words, num_entities, 3] (start/end/inside)
|
|
327
|
+
const rawSpans = [];
|
|
328
|
+
const selectedStarts = [];
|
|
329
|
+
const selectedEnds = [];
|
|
330
|
+
const insideScores = Array.from({ length: textLength }, () => Array(numEntities).fill(0));
|
|
331
|
+
for (let token = 0; token < textLength; token++) {
|
|
332
|
+
for (let entity = 0; entity < numEntities; entity++) {
|
|
333
|
+
const base = (token * numEntities + entity) * 3;
|
|
334
|
+
const startProb = sigmoid(logits[base]);
|
|
335
|
+
const endProb = sigmoid(logits[base + 1]);
|
|
336
|
+
const insideProb = sigmoid(logits[base + 2]);
|
|
337
|
+
if (startProb >= this.threshold)
|
|
338
|
+
selectedStarts.push([token, entity]);
|
|
339
|
+
if (endProb >= this.threshold)
|
|
340
|
+
selectedEnds.push([token, entity]);
|
|
341
|
+
insideScores[token][entity] = insideProb;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const [startTok, startCls] of selectedStarts) {
|
|
345
|
+
for (const [endTok, endCls] of selectedEnds) {
|
|
346
|
+
if (endTok < startTok || startCls !== endCls)
|
|
347
|
+
continue;
|
|
348
|
+
const inside = insideScores.slice(startTok, endTok + 1).map(s => s[startCls]);
|
|
349
|
+
if (inside.some(s => s < this.threshold))
|
|
350
|
+
continue;
|
|
351
|
+
const score = inside.reduce((a, b) => a + b, 0) / inside.length;
|
|
352
|
+
const charStart = charStarts[startTok];
|
|
353
|
+
const charEnd = charEnds[endTok];
|
|
354
|
+
const spanText = fullText.slice(charStart, charEnd);
|
|
355
|
+
rawSpans.push([spanText, charStart, charEnd, idToClass[startCls + 1], score]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return rawSpans;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type { RegexRule, PiiDomain, RegionCode } from './types.js';
|
|
2
|
+
export type { RegexRuleJson, RegionRulesJson } from './loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* Region choices for the regex detector: 'all' plus every shipped region
|
|
5
|
+
* pack. Order is significant - hosts render region pickers straight from
|
|
6
|
+
* this list (moved verbatim from the web app's engine.ts in T009).
|
|
7
|
+
*/
|
|
8
|
+
export declare const REGEX_REGIONS: readonly ["all", "gb", "us", "pl", "de", "fr", "es", "pt", "se", "no", "it", "nl", "be", "at", "ch", "ie", "dk", "fi", "jp", "cn"];
|
|
9
|
+
export type RegexRegionId = typeof REGEX_REGIONS[number];
|
|
10
|
+
import type { RegexRule } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* All regex rules, compiled from the canonical rules/*.json packs (T018).
|
|
13
|
+
* The JSON is embedded at authoring time via the generated rules.data.ts;
|
|
14
|
+
* pack order there matches the pre-T018 hand-written concatenation order.
|
|
15
|
+
* An unknown validator name in the data fails right here, at module init.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ALL_REGEX_RULES: RegexRule[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region choices for the regex detector: 'all' plus every shipped region
|
|
3
|
+
* pack. Order is significant - hosts render region pickers straight from
|
|
4
|
+
* this list (moved verbatim from the web app's engine.ts in T009).
|
|
5
|
+
*/
|
|
6
|
+
export const REGEX_REGIONS = [
|
|
7
|
+
'all', 'gb', 'us', 'pl', 'de', 'fr', 'es', 'pt', 'se', 'no',
|
|
8
|
+
'it', 'nl', 'be', 'at', 'ch', 'ie', 'dk', 'fi', 'jp', 'cn',
|
|
9
|
+
];
|
|
10
|
+
import { loadRegexRules } from "./loader.js";
|
|
11
|
+
import { RULES_DATA } from "./rules.data.js";
|
|
12
|
+
/**
|
|
13
|
+
* All regex rules, compiled from the canonical rules/*.json packs (T018).
|
|
14
|
+
* The JSON is embedded at authoring time via the generated rules.data.ts;
|
|
15
|
+
* pack order there matches the pre-T018 hand-written concatenation order.
|
|
16
|
+
* An unknown validator name in the data fails right here, at module init.
|
|
17
|
+
*/
|
|
18
|
+
export const ALL_REGEX_RULES = loadRegexRules(RULES_DATA);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the shared rules format (T018, arch doc section 6).
|
|
3
|
+
*
|
|
4
|
+
* Turns the canonical rules/*.json data (embedded at authoring time as
|
|
5
|
+
* rules.data.ts) into runtime RegexRule objects: compiles each pattern with
|
|
6
|
+
* its declared flags and resolves `validate` names against the validator
|
|
7
|
+
* registry. An unknown validator name throws, which surfaces at module init
|
|
8
|
+
* time because index.ts calls loadRegexRules() at module scope.
|
|
9
|
+
*/
|
|
10
|
+
import type { EntityType } from '../types.js';
|
|
11
|
+
import type { PiiDomain, RegexRule, RegionCode } from './types.js';
|
|
12
|
+
/** JSON shape of a single rule inside a rules/<region>.json pack. */
|
|
13
|
+
export interface RegexRuleJson {
|
|
14
|
+
/** Stable detector identifier, e.g. 'regex:pl:pesel' */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Entity type the rule detects */
|
|
17
|
+
entityType: EntityType;
|
|
18
|
+
/** Regex source (ECMAScript/Python common subset) */
|
|
19
|
+
pattern: string;
|
|
20
|
+
/** Regex flags; must include 'g' */
|
|
21
|
+
flags: string;
|
|
22
|
+
/** Default confidence score (0-1) */
|
|
23
|
+
confidence: number;
|
|
24
|
+
/** Domain tags for categorization/filtering */
|
|
25
|
+
domains: PiiDomain[];
|
|
26
|
+
/** Human-readable description for developers maintaining rules */
|
|
27
|
+
description: string;
|
|
28
|
+
/** Example strings that SHOULD match (used in auto-generated tests) */
|
|
29
|
+
examples?: string[];
|
|
30
|
+
/** Known false-positive patterns or notes */
|
|
31
|
+
falsePositiveNotes?: string;
|
|
32
|
+
/** Name of a post-match validation function in the validator registry */
|
|
33
|
+
validate?: string;
|
|
34
|
+
}
|
|
35
|
+
/** JSON shape of a whole rules/<region>.json file. */
|
|
36
|
+
export interface RegionRulesJson {
|
|
37
|
+
region: RegionCode;
|
|
38
|
+
rules: readonly RegexRuleJson[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Compile rule packs into the flat RegexRule list, preserving pack order and
|
|
42
|
+
* in-pack rule order (the concatenation order defines ALL_REGEX_RULES).
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadRegexRules(packs: readonly RegionRulesJson[]): RegexRule[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { VALIDATORS } from "./validators.js";
|
|
2
|
+
function compileRule(region, json) {
|
|
3
|
+
let pattern;
|
|
4
|
+
try {
|
|
5
|
+
pattern = new RegExp(json.pattern, json.flags);
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
throw new Error(`Rule ${json.id}: pattern does not compile with flags "${json.flags}": ${String(err)}`);
|
|
9
|
+
}
|
|
10
|
+
const rule = {
|
|
11
|
+
pattern,
|
|
12
|
+
type: json.entityType,
|
|
13
|
+
detector: json.id,
|
|
14
|
+
confidence: json.confidence,
|
|
15
|
+
region,
|
|
16
|
+
domains: json.domains,
|
|
17
|
+
description: json.description,
|
|
18
|
+
};
|
|
19
|
+
if (json.examples !== undefined)
|
|
20
|
+
rule.examples = json.examples;
|
|
21
|
+
if (json.falsePositiveNotes !== undefined)
|
|
22
|
+
rule.falsePositiveNotes = json.falsePositiveNotes;
|
|
23
|
+
if (json.validate !== undefined) {
|
|
24
|
+
const validate = VALIDATORS[json.validate];
|
|
25
|
+
if (validate === undefined) {
|
|
26
|
+
throw new Error(`Rule ${json.id}: unknown validator "${json.validate}" (not in the validators.ts registry)`);
|
|
27
|
+
}
|
|
28
|
+
rule.validate = validate;
|
|
29
|
+
}
|
|
30
|
+
return rule;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compile rule packs into the flat RegexRule list, preserving pack order and
|
|
34
|
+
* in-pack rule order (the concatenation order defines ALL_REGEX_RULES).
|
|
35
|
+
*/
|
|
36
|
+
export function loadRegexRules(packs) {
|
|
37
|
+
const rules = [];
|
|
38
|
+
for (const pack of packs) {
|
|
39
|
+
for (const json of pack.rules) {
|
|
40
|
+
rules.push(compileRule(pack.region, json));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return rules;
|
|
44
|
+
}
|