@doccloak/core 0.11.0 → 0.12.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +121 -2
  2. package/README.md +252 -19
  3. package/dist/doc.d.ts +68 -1
  4. package/dist/doc.js +447 -62
  5. package/dist/dom/docx.d.ts +135 -7
  6. package/dist/dom/docx.js +752 -125
  7. package/dist/dom/drawingml.d.ts +59 -0
  8. package/dist/dom/drawingml.js +176 -0
  9. package/dist/dom/errors.d.ts +35 -0
  10. package/dist/dom/errors.js +24 -0
  11. package/dist/dom/index.d.ts +3 -0
  12. package/dist/dom/index.js +6 -0
  13. package/dist/dom/office.d.ts +56 -0
  14. package/dist/dom/office.js +76 -23
  15. package/dist/dom/opc.d.ts +135 -7
  16. package/dist/dom/opc.js +407 -14
  17. package/dist/dom/package-policy.d.ts +137 -0
  18. package/dist/dom/package-policy.js +355 -0
  19. package/dist/dom/xlsx.d.ts +64 -6
  20. package/dist/dom/xlsx.js +75 -20
  21. package/dist/engine.d.ts +18 -0
  22. package/dist/engine.js +37 -13
  23. package/dist/env.d.ts +21 -1
  24. package/dist/index.d.ts +8 -6
  25. package/dist/index.js +10 -5
  26. package/dist/model-loader.d.ts +61 -6
  27. package/dist/model-loader.js +122 -17
  28. package/dist/pipeline.d.ts +87 -8
  29. package/dist/pipeline.js +249 -50
  30. package/dist/providers/bardsai.d.ts +8 -1
  31. package/dist/providers/bardsai.js +22 -2
  32. package/dist/providers/gliner-base.d.ts +8 -1
  33. package/dist/providers/gliner-base.js +22 -2
  34. package/dist/providers/gliner.d.ts +8 -1
  35. package/dist/providers/gliner.js +22 -2
  36. package/dist/regex/loader.d.ts +2 -0
  37. package/dist/regex/loader.js +2 -0
  38. package/dist/regex/rules.data.js +15 -14
  39. package/dist/regex/types.d.ts +6 -0
  40. package/dist/session.d.ts +101 -3
  41. package/dist/session.js +397 -43
  42. package/dist/surrogates.js +57 -45
  43. package/dist/worker-protocol.d.ts +13 -1
  44. package/dist/worker-protocol.js +164 -12
  45. package/package.json +1 -1
  46. package/rules/at.json +1 -1
  47. package/rules/be.json +1 -1
  48. package/rules/ch.json +1 -1
  49. package/rules/de.json +1 -1
  50. package/rules/dk.json +1 -1
  51. package/rules/es.json +1 -1
  52. package/rules/fi.json +1 -1
  53. package/rules/fr.json +1 -1
  54. package/rules/it.json +1 -1
  55. package/rules/no.json +1 -1
  56. package/rules/pl.json +1 -1
  57. package/rules/pt.json +1 -1
  58. package/rules/schema.json +4 -0
  59. package/rules/se.json +1 -1
  60. package/rules/universal.json +2 -1
@@ -82,36 +82,48 @@ export function generateSessionSalt() {
82
82
  return out;
83
83
  }
84
84
  // ── Name pools (EN + PL) ───────────────────────────────────
85
+ //
86
+ // Pool hygiene (T182, audit R3/R4): a surrogate becomes a restore KEY, so
87
+ // every alphabetic pool excludes (a) names that are also everyday words
88
+ // in English or, for capitalized-noun languages, in their own language
89
+ // (White, Green, King, Young, Hill, Baker, Post, Wolf, Richter, Urban,
90
+ // Martin the bird, ...) and (b) entries of three letters or fewer (Bo,
91
+ // Per, Odd, Jan, Eva, Bos, Vos, Kok, ...), which fire inside ordinary
92
+ // words even with boundaries around them. Replacements keep the pool
93
+ // size and sit at the same index, so surrogate derivation is unchanged
94
+ // for every original that never picked a removed entry. CJK pools are
95
+ // exempt from (b): their names are one to three characters by design.
96
+ // The pool-hygiene test in tests/surrogates.test.ts enforces both rules.
85
97
  const EN_FIRST_MALE = [
86
98
  'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph',
87
- 'Thomas', 'Charles', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Steven', 'Paul',
99
+ 'Thomas', 'Charles', 'Daniel', 'Matthew', 'Anthony', 'Dennis', 'Steven', 'Paul',
88
100
  'Andrew', 'Joshua', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward',
89
101
  'Jason', 'Jeffrey', 'Ryan', 'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan',
90
- 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon', 'Benjamin', 'Samuel', 'Gregory',
102
+ 'Stephen', 'Larry', 'Justin', 'Douglas', 'Brandon', 'Benjamin', 'Samuel', 'Gregory',
91
103
  ];
92
104
  const EN_FIRST_FEMALE = [
93
105
  'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica',
94
106
  'Sarah', 'Karen', 'Lisa', 'Nancy', 'Betty', 'Margaret', 'Sandra', 'Ashley',
95
- 'Kimberly', 'Emily', 'Donna', 'Michelle', 'Carol', 'Amanda', 'Dorothy', 'Melissa',
96
- 'Deborah', 'Stephanie', 'Rebecca', 'Sharon', 'Laura', 'Cynthia', 'Kathleen', 'Amy',
107
+ 'Kimberly', 'Emily', 'Donna', 'Michelle', 'Janet', 'Amanda', 'Dorothy', 'Melissa',
108
+ 'Deborah', 'Stephanie', 'Rebecca', 'Sharon', 'Laura', 'Cynthia', 'Kathleen', 'Kathryn',
97
109
  'Angela', 'Shirley', 'Anna', 'Ruth', 'Brenda', 'Pamela', 'Emma', 'Nicole',
98
110
  ];
99
111
  const EN_SURNAMES = [
100
- 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
101
- 'Wilson', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', 'Martin', 'Lee',
102
- 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson', 'Walker', 'Young',
103
- 'Allen', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams', 'Nelson',
104
- 'Hill', 'Campbell', 'Mitchell', 'Roberts', 'Carter', 'Phillips', 'Evans', 'Turner',
112
+ 'Smith', 'Johnson', 'Williams', 'Collins', 'Jones', 'Garcia', 'Stewart', 'Davis',
113
+ 'Wilson', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', 'Morgan', 'Murphy',
114
+ 'Thompson', 'Rogers', 'Harris', 'Clark', 'Lewis', 'Robinson', 'Peterson', 'Sullivan',
115
+ 'Allen', 'Bennett', 'Wright', 'Hughes', 'Howard', 'Richardson', 'Adams', 'Nelson',
116
+ 'Morris', 'Campbell', 'Mitchell', 'Roberts', 'Carter', 'Phillips', 'Evans', 'Turner',
105
117
  ];
106
118
  const PL_FIRST_MALE = [
107
- 'Jan', 'Piotr', 'Krzysztof', 'Andrzej', 'Tomasz', 'Paweł', 'Michał', 'Marcin',
119
+ 'Stanisław', 'Piotr', 'Krzysztof', 'Andrzej', 'Tomasz', 'Paweł', 'Michał', 'Marcin',
108
120
  'Marek', 'Grzegorz', 'Jerzy', 'Tadeusz', 'Adam', 'Łukasz', 'Zbigniew', 'Ryszard',
109
121
  'Dariusz', 'Henryk', 'Mariusz', 'Kazimierz', 'Wojciech', 'Robert', 'Mateusz', 'Marian',
110
122
  'Rafał', 'Jacek', 'Janusz', 'Mirosław', 'Maciej', 'Sławomir', 'Jarosław', 'Kamil',
111
- 'Wiesław', 'Roman', 'Władysław', 'Jakub', 'Artur', 'Zdzisław', 'Edward', 'Dawid',
123
+ 'Wiesław', 'Bogdan', 'Władysław', 'Jakub', 'Artur', 'Zdzisław', 'Edward', 'Dawid',
112
124
  ];
113
125
  const PL_FIRST_FEMALE = [
114
- 'Anna', 'Maria', 'Katarzyna', 'Małgorzata', 'Agnieszka', 'Barbara', 'Krystyna', 'Ewa',
126
+ 'Anna', 'Maria', 'Katarzyna', 'Małgorzata', 'Agnieszka', 'Barbara', 'Krystyna', 'Weronika',
115
127
  'Elżbieta', 'Zofia', 'Janina', 'Teresa', 'Joanna', 'Magdalena', 'Monika', 'Jadwiga',
116
128
  'Danuta', 'Irena', 'Halina', 'Helena', 'Beata', 'Aleksandra', 'Marta', 'Dorota',
117
129
  'Marianna', 'Grażyna', 'Jolanta', 'Stanisława', 'Iwona', 'Karolina', 'Bożena', 'Urszula',
@@ -121,8 +133,8 @@ const PL_FIRST_FEMALE = [
121
133
  const PL_SURNAMES = [
122
134
  'Nowak', 'Kowalski', 'Wiśniewski', 'Wójcik', 'Kowalczyk', 'Kamiński', 'Lewandowski', 'Zieliński',
123
135
  'Szymański', 'Woźniak', 'Dąbrowski', 'Kozłowski', 'Jankowski', 'Mazur', 'Kwiatkowski', 'Krawczyk',
124
- 'Piotrowski', 'Grabowski', 'Nowakowski', 'Pawłowski', 'Michalski', 'Nowicki', 'Adamczyk', 'Dudek',
125
- 'Zając', 'Wieczorek', 'Jabłoński', 'Król', 'Majewski', 'Olszewski', 'Jaworski', 'Wróbel',
136
+ 'Piotrowski', 'Grabowski', 'Nowakowski', 'Pawłowski', 'Michalski', 'Nowicki', 'Adamczyk', 'Kaczmarek',
137
+ 'Sobczak', 'Wieczorek', 'Jabłoński', 'Zalewski', 'Majewski', 'Olszewski', 'Jaworski', 'Zawadzki',
126
138
  'Malinowski', 'Pawlak', 'Witkowski', 'Walczak', 'Stępień', 'Górski', 'Rutkowski', 'Michalak',
127
139
  ];
128
140
  function feminizePlSurname(surname) {
@@ -143,7 +155,7 @@ function feminizePlSurname(surname) {
143
155
  // guaranteed by collision handling, not pool size.
144
156
  const DE_FIRST_MALE = [
145
157
  'Hans', 'Peter', 'Michael', 'Thomas', 'Andreas', 'Wolfgang', 'Klaus', 'Jürgen',
146
- 'Stefan', 'Christian', 'Markus', 'Alexander', 'Frank', 'Uwe', 'Martin', 'Werner',
158
+ 'Stefan', 'Christian', 'Markus', 'Alexander', 'Dieter', 'Rainer', 'Holger', 'Werner',
147
159
  'Matthias', 'Bernd', 'Florian', 'Tobias', 'Sebastian', 'Lukas', 'Felix', 'Jonas',
148
160
  ];
149
161
  const DE_FIRST_FEMALE = [
@@ -152,8 +164,8 @@ const DE_FIRST_FEMALE = [
152
164
  'Christina', 'Birgit', 'Heike', 'Lena', 'Hannah', 'Laura', 'Lisa', 'Sophie',
153
165
  ];
154
166
  const DE_SURNAMES = [
155
- 'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Weber', 'Meyer', 'Wagner', 'Becker',
156
- 'Schulz', 'Hoffmann', 'Schäfer', 'Koch', 'Bauer', 'Richter', 'Klein', 'Wolf',
167
+ 'Müller', 'Schmidt', 'Schneider', 'Herrmann', 'Weber', 'Meyer', 'Wagner', 'Becker',
168
+ 'Schulz', 'Hoffmann', 'Schäfer', 'Lehmann', 'Köhler', 'Schulze', 'Klein', 'Hartmann',
157
169
  'Schröder', 'Neumann', 'Schwarz', 'Zimmermann', 'Braun', 'Krüger', 'Hofmann', 'Lange',
158
170
  ];
159
171
  const FR_FIRST_MALE = [
@@ -163,13 +175,13 @@ const FR_FIRST_MALE = [
163
175
  ];
164
176
  const FR_FIRST_FEMALE = [
165
177
  'Marie', 'Nathalie', 'Isabelle', 'Sylvie', 'Catherine', 'Françoise', 'Christine', 'Monique',
166
- 'Sophie', 'Céline', 'Julie', 'Aurélie', 'Camille', 'Léa', 'Chloé', 'Manon',
178
+ 'Sophie', 'Céline', 'Julie', 'Aurélie', 'Camille', 'Mathilde', 'Chloé', 'Manon',
167
179
  'Élise', 'Charlotte', 'Emma', 'Louise', 'Alice', 'Juliette', 'Margaux', 'Inès',
168
180
  ];
169
181
  const FR_SURNAMES = [
170
- 'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand',
182
+ 'Mercier', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Rousseau', 'Durand',
171
183
  'Leroy', 'Moreau', 'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David',
172
- 'Bertrand', 'Roux', 'Vincent', 'Fournier', 'Morel', 'Girard', 'Lambert', 'Fontaine',
184
+ 'Bertrand', 'Garnier', 'Vincent', 'Fournier', 'Morel', 'Girard', 'Lambert', 'Fontaine',
173
185
  ];
174
186
  const ES_FIRST_MALE = [
175
187
  'José', 'Antonio', 'Manuel', 'Francisco', 'Juan', 'David', 'Javier', 'Carlos',
@@ -177,7 +189,7 @@ const ES_FIRST_MALE = [
177
189
  'Jorge', 'Alberto', 'Diego', 'Adrián', 'Raúl', 'Iván', 'Rubén', 'Óscar',
178
190
  ];
179
191
  const ES_FIRST_FEMALE = [
180
- 'María', 'Carmen', 'Josefa', 'Isabel', 'Ana', 'Dolores', 'Pilar', 'Teresa',
192
+ 'María', 'Carmen', 'Josefa', 'Isabel', 'Alicia', 'Dolores', 'Pilar', 'Teresa',
181
193
  'Rosa', 'Cristina', 'Laura', 'Marta', 'Elena', 'Lucía', 'Sara', 'Paula',
182
194
  'Raquel', 'Beatriz', 'Silvia', 'Patricia', 'Nuria', 'Alba', 'Andrea', 'Irene',
183
195
  ];
@@ -203,19 +215,19 @@ const IT_SURNAMES = [
203
215
  ];
204
216
  /** Masculine base forms; feminizeCsSurname derives -ová / -á endings. */
205
217
  const CS_FIRST_MALE = [
206
- 'Jiří', 'Jan', 'Petr', 'Josef', 'Pavel', 'Martin', 'Tomáš', 'Jaroslav',
218
+ 'Jiří', 'Vojtěch', 'Petr', 'Josef', 'Pavel', 'Matěj', 'Tomáš', 'Jaroslav',
207
219
  'Miroslav', 'Zdeněk', 'František', 'Václav', 'Michal', 'Milan', 'Karel', 'Lukáš',
208
- 'David', 'Ladislav', 'Stanislav', 'Roman', 'Ondřej', 'Jakub', 'Vladimír', 'Radek',
220
+ 'David', 'Ladislav', 'Stanislav', 'Libor', 'Ondřej', 'Jakub', 'Vladimír', 'Radek',
209
221
  ];
210
222
  const CS_FIRST_FEMALE = [
211
223
  'Marie', 'Jiřina', 'Anna', 'Věra', 'Alena', 'Lenka', 'Hana', 'Jaroslava',
212
- 'Kateřina', 'Lucie', 'Eva', 'Jana', 'Petra', 'Martina', 'Zuzana', 'Michaela',
224
+ 'Kateřina', 'Lucie', 'Simona', 'Jana', 'Petra', 'Martina', 'Zuzana', 'Michaela',
213
225
  'Tereza', 'Barbora', 'Veronika', 'Kristýna', 'Markéta', 'Ivana', 'Monika', 'Klára',
214
226
  ];
215
227
  const CS_SURNAMES = [
216
228
  'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházka', 'Kučera', 'Veselý',
217
- 'Horák', 'Němec', 'Marek', 'Pokorný', 'Pospíšil', 'Hájek', 'Král', 'Jelínek',
218
- 'Růžička', 'Beneš', 'Fiala', 'Sedláček', 'Doležal', 'Zeman', 'Kolář', 'Urban',
229
+ 'Horák', 'Němec', 'Marek', 'Pokorný', 'Pospíšil', 'Hájek', 'Bartoš', 'Jelínek',
230
+ 'Růžička', 'Beneš', 'Fiala', 'Sedláček', 'Doležal', 'Zeman', 'Kolář', 'Šimek',
219
231
  ];
220
232
  function feminizeCsSurname(surname) {
221
233
  if (surname.endsWith('ý'))
@@ -241,31 +253,31 @@ const UK_SURNAMES = [
241
253
  'Мороз', 'Гончаренко', 'Левченко', 'Василенко', 'Кушнір', 'Романюк', 'Гаврилюк', 'Заєць',
242
254
  ];
243
255
  const NL_FIRST_MALE = [
244
- 'Daan', 'Sem', 'Bram', 'Lars', 'Thijs', 'Ruben', 'Kees', 'Joost',
245
- 'Maarten', 'Wouter', 'Jeroen', 'Sander', 'Bas', 'Niels', 'Floris', 'Gerrit',
256
+ 'Daan', 'Luuk', 'Bram', 'Lars', 'Thijs', 'Ruben', 'Kees', 'Joost',
257
+ 'Maarten', 'Wouter', 'Jeroen', 'Sander', 'Stijn', 'Niels', 'Floris', 'Gerrit',
246
258
  'Pieter', 'Willem', 'Hendrik', 'Cornelis', 'Dirk', 'Jaap', 'Sjoerd', 'Teun',
247
259
  ];
248
260
  const NL_FIRST_FEMALE = [
249
- 'Sanne', 'Lotte', 'Femke', 'Anouk', 'Fleur', 'Iris', 'Nienke', 'Marloes',
261
+ 'Sanne', 'Lotte', 'Femke', 'Anouk', 'Fleur', 'Saskia', 'Nienke', 'Marloes',
250
262
  'Ilse', 'Esmee', 'Roos', 'Lieke', 'Johanna', 'Cornelia', 'Willemien', 'Truus',
251
- 'Marijke', 'Annemiek', 'Wilma', 'Jantine', 'Gerda', 'Hanneke', 'Mieke', 'Els',
263
+ 'Marijke', 'Annemiek', 'Wilma', 'Jantine', 'Gerda', 'Hanneke', 'Mieke', 'Annelies',
252
264
  ];
253
265
  /** Single-token forms only: multi-token surnames (van der ...) would
254
266
  * break the token-count preservation guarantee of generatePerson. */
255
267
  const NL_SURNAMES = [
256
- 'Jansen', 'Bakker', 'Visser', 'Smit', 'Meijer', 'Mulder', 'Bos', 'Vos',
257
- 'Peters', 'Hendriks', 'Dekker', 'Brouwer', 'Dijkstra', 'Smits', 'Kuipers', 'Post',
258
- 'Kok', 'Verhoeven', 'Willems', 'Maas', 'Hermans', 'Timmermans', 'Schouten', 'Jacobs',
268
+ 'Jansen', 'Bakker', 'Visser', 'Smit', 'Meijer', 'Mulder', 'Vermeulen', 'Koster',
269
+ 'Peters', 'Hendriks', 'Dekker', 'Brouwer', 'Dijkstra', 'Smits', 'Kuipers', 'Hoekstra',
270
+ 'Scholten', 'Verhoeven', 'Willems', 'Maas', 'Hermans', 'Timmermans', 'Schouten', 'Jacobs',
259
271
  ];
260
272
  const PT_FIRST_MALE = [
261
- 'Jo\u00e3o', 'Lu\u00eds', 'Paulo', 'Rui', 'Nuno', 'Tiago', 'Ricardo', 'Andr\u00e9',
273
+ 'Jo\u00e3o', 'Lu\u00eds', 'Paulo', 'Guilherme', 'Nuno', 'Tiago', 'Ricardo', 'Andr\u00e9',
262
274
  'Diogo', 'Gon\u00e7alo', 'Vasco', 'Duarte', 'Afonso', 'Bernardo', 'Rodrigo', 'Ant\u00f3nio',
263
275
  'Manuel', 'Francisco', 'Carlos', 'Pedro', 'Miguel', 'Jos\u00e9', 'Henrique', 'Sim\u00e3o',
264
276
  ];
265
277
  const PT_FIRST_FEMALE = [
266
278
  'Catarina', 'Margarida', 'Mariana', 'Joana', 'Rita', 'Carolina', 'Leonor', 'Matilde',
267
279
  'Francisca', 'Madalena', 'Const\u00e2ncia', 'Louren\u00e7a', 'Gra\u00e7a', 'Concei\u00e7\u00e3o', 'Lurdes', 'In\u00eas',
268
- 'Beatriz', 'Isabel', 'Teresa', 'Sofia', 'Ana', 'Maria', 'Manuela', 'Fernanda',
280
+ 'Beatriz', 'Isabel', 'Teresa', 'Sofia', 'Filipa', 'Maria', 'Manuela', 'Fernanda',
269
281
  ];
270
282
  const PT_SURNAMES = [
271
283
  'Silva', 'Santos', 'Ferreira', 'Pereira', 'Oliveira', 'Rodrigues', 'Martins', 'Sousa',
@@ -279,11 +291,11 @@ function feminizeUkSurname(surname) {
279
291
  return surname; // -енко/-ук/-як and similar are invariant
280
292
  }
281
293
  const SV_FIRST_MALE = [
282
- 'Erik', 'Lars', 'Karl', 'Anders', 'Johan', 'Per', 'Nils', 'Sven',
283
- 'Gunnar', 'Bo', '\u00c5ke', 'G\u00f6ran', 'Henrik', 'Magnus', 'Fredrik', 'Oskar',
294
+ 'Erik', 'Lars', 'Karl', 'Anders', 'Johan', 'Mats', 'Nils', 'Sven',
295
+ 'Gunnar', 'Olof', 'Bengt', 'G\u00f6ran', 'Henrik', 'Magnus', 'Fredrik', 'Oskar',
284
296
  ];
285
297
  const SV_FIRST_FEMALE = [
286
- 'Anna', 'Eva', 'Maria', 'Karin', 'Ingrid', 'Kerstin', 'Lena', 'Helena',
298
+ 'Anna', 'Kristina', 'Maria', 'Karin', 'Ingrid', 'Kerstin', 'Lena', 'Helena',
287
299
  'Marianne', 'Birgitta', 'Elin', 'Sara', 'Emma', 'Linnea', 'Astrid', 'Ebba',
288
300
  ];
289
301
  const SV_SURNAMES = [
@@ -291,23 +303,23 @@ const SV_SURNAMES = [
291
303
  'Svensson', 'Gustafsson', 'Pettersson', 'Jonsson', 'Lindberg', 'Lindqvist', 'Bergstr\u00f6m', 'Sandberg',
292
304
  ];
293
305
  const NO_FIRST_MALE = [
294
- 'Ole', 'Lars', 'Knut', 'Bj\u00f8rn', 'Arne', 'Odd', 'Geir', 'Tor',
306
+ 'Olav', 'Lars', 'Knut', 'Bj\u00f8rn', 'Arne', 'Einar', 'Geir', 'Sverre',
295
307
  'Terje', 'Kjell', 'Espen', 'H\u00e5kon', 'Sindre', 'Eirik', 'Trygve', 'Leif',
296
308
  ];
297
309
  const NO_FIRST_FEMALE = [
298
- 'Anne', 'Inger', 'Kari', 'Marit', 'Ingrid', 'Liv', 'Astrid', 'Solveig',
299
- 'Randi', 'Bj\u00f8rg', 'Silje', 'Mari', 'Ingeborg', 'Tone', 'Gunn', 'Sigrid',
310
+ 'Anne', 'Inger', 'Kari', 'Marit', 'Ingrid', 'Hilde', 'Astrid', 'Solveig',
311
+ 'Randi', 'Bj\u00f8rg', 'Silje', 'Mari', 'Ingeborg', 'Berit', 'Gunn', 'Sigrid',
300
312
  ];
301
313
  const NO_SURNAMES = [
302
314
  'Hansen', 'Johansen', 'Olsen', 'Larsen', 'Andersen', 'Pedersen', 'Nilsen', 'Kristiansen',
303
- 'Jensen', 'Karlsen', 'Berg', 'Haugen', 'Hagen', 'Solberg', 'Moen', 'Lien',
315
+ 'Jensen', 'Karlsen', 'Berg', 'Haugen', 'Hagen', 'Solberg', 'Moen', 'Eide',
304
316
  ];
305
317
  const DA_FIRST_MALE = [
306
- 'Jens', 'Peter', 'Lars', 'Henrik', 'S\u00f8ren', 'Niels', 'Ole', 'Erik',
307
- 'Mads', 'Rasmus', 'Bent', 'Kaj', 'Frederik', 'Mikkel', 'Emil', 'Bjarne',
318
+ 'Jens', 'Peter', 'Lars', 'Henrik', 'S\u00f8ren', 'Niels', 'Torben', 'Erik',
319
+ 'Mads', 'Rasmus', 'Jesper', 'Morten', 'Frederik', 'Mikkel', 'Emil', 'Bjarne',
308
320
  ];
309
321
  const DA_FIRST_FEMALE = [
310
- 'Kirsten', 'Mette', 'Hanne', 'Lone', 'Bente', 'Karen', 'Dorthe', 'Pia',
322
+ 'Kirsten', 'Mette', 'Hanne', 'Trine', 'Bente', 'Karen', 'Dorthe', 'Helle',
311
323
  'Gitte', 'Ditte', 'Freja', 'Signe', 'Maja', 'Cecilie', 'Louise', 'Astrid',
312
324
  ];
313
325
  const DA_SURNAMES = [
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import type { DetectedEntity } from './types.js';
23
23
  import type { DocCloakEngine, EngineSettings, ProviderId } from './engine.js';
24
- import type { RegexRegionId } from './regex/index.js';
24
+ import { type RegexRegionId } from './regex/index.js';
25
25
  /** Satisfied by Worker, MessagePort and chrome.runtime.Port wrappers. */
26
26
  export interface PortLike {
27
27
  postMessage(msg: unknown): void;
@@ -90,6 +90,18 @@ export type EngineResponse = {
90
90
  } | {
91
91
  type: 'released';
92
92
  requestId?: number;
93
+ }
94
+ /**
95
+ * Protocol-level failure (T185, R12): a message the host could not act on
96
+ * (unknown type, wrong payload shape) or an error outside the dedicated
97
+ * load/detect replies. `fatal` marks a host that can no longer serve; the
98
+ * client then rejects every pending request and closes.
99
+ */
100
+ | {
101
+ type: 'error';
102
+ requestId?: number;
103
+ error: string;
104
+ fatal?: boolean;
93
105
  };
94
106
  /**
95
107
  * Serve a real engine over a port. Returns a stop function that
@@ -19,10 +19,83 @@
19
19
  * is the client side: a DocCloakEngine that forwards over the port (web
20
20
  * main thread, extension side panel / service worker).
21
21
  */
22
- import { clampThreshold, defaultThresholdFor } from "./engine.js";
22
+ import { PROVIDERS, clampThreshold, defaultThresholdFor } from "./engine.js";
23
+ import { REGEX_REGIONS } from "./regex/index.js";
23
24
  function errorMessage(err) {
24
25
  return err instanceof Error ? err.message : String(err);
25
26
  }
27
+ // ── Message validation (R12) ───────────────────────────────
28
+ const REQUEST_TYPES = new Set([
29
+ 'init', 'detect', 'switchProvider', 'setThreshold', 'setCustomLabels', 'setRegex', 'setRegexRegion', 'releaseModel',
30
+ ]);
31
+ function isRecord(value) {
32
+ return typeof value === 'object' && value !== null;
33
+ }
34
+ function isProviderId(value) {
35
+ return typeof value === 'string' && PROVIDERS.some((p) => p.id === value);
36
+ }
37
+ function isRegexRegionId(value) {
38
+ return typeof value === 'string' && REGEX_REGIONS.includes(value);
39
+ }
40
+ function isStringArray(value) {
41
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
42
+ }
43
+ function isFiniteNumber(value) {
44
+ return typeof value === 'number' && Number.isFinite(value);
45
+ }
46
+ function optionalRequestId(value) {
47
+ return isFiniteNumber(value) ? value : undefined;
48
+ }
49
+ /**
50
+ * Field-level validation of a message whose `type` is known. Returns a
51
+ * description of the first problem, or null when the payload is usable.
52
+ */
53
+ function requestProblem(msg) {
54
+ switch (msg.type) {
55
+ case 'init':
56
+ if (msg.providerId !== undefined && !isProviderId(msg.providerId))
57
+ return 'providerId must be a known provider id';
58
+ if (msg.customLabels !== undefined && !isStringArray(msg.customLabels))
59
+ return 'customLabels must be an array of strings';
60
+ if (msg.regexEnabled !== undefined && typeof msg.regexEnabled !== 'boolean')
61
+ return 'regexEnabled must be a boolean';
62
+ if (msg.regexRegion !== undefined && !isRegexRegionId(msg.regexRegion))
63
+ return 'regexRegion must be a known region id';
64
+ return null;
65
+ case 'detect':
66
+ if (!isFiniteNumber(msg.requestId))
67
+ return 'requestId must be a number';
68
+ if (typeof msg.text !== 'string')
69
+ return 'text must be a string';
70
+ return null;
71
+ case 'switchProvider':
72
+ if (!isProviderId(msg.providerId))
73
+ return 'providerId must be a known provider id';
74
+ if (msg.customLabels !== undefined && !isStringArray(msg.customLabels))
75
+ return 'customLabels must be an array of strings';
76
+ return null;
77
+ case 'setThreshold':
78
+ if (!isFiniteNumber(msg.value))
79
+ return 'value must be a finite number';
80
+ return null;
81
+ case 'setCustomLabels':
82
+ if (!isStringArray(msg.labels))
83
+ return 'labels must be an array of strings';
84
+ return null;
85
+ case 'setRegex':
86
+ if (typeof msg.enabled !== 'boolean')
87
+ return 'enabled must be a boolean';
88
+ if (msg.region !== undefined && !isRegexRegionId(msg.region))
89
+ return 'region must be a known region id';
90
+ return null;
91
+ case 'setRegexRegion':
92
+ if (!isRegexRegionId(msg.region))
93
+ return 'region must be a known region id';
94
+ return null;
95
+ case 'releaseModel':
96
+ return null;
97
+ }
98
+ }
26
99
  // ── Host side ──────────────────────────────────────────────
27
100
  /**
28
101
  * Serve a real engine over a port. Returns a stop function that
@@ -46,8 +119,37 @@ export function serveEngine(engine, port) {
46
119
  post({ type: 'loadError', requestId, error: errorMessage(err) });
47
120
  }
48
121
  }
49
- const unsubscribeMessages = port.onMessage(async (raw) => {
50
- const msg = raw;
122
+ /** Fire-and-forget settings message: failures are reported, never thrown. */
123
+ async function handleSetting(run) {
124
+ try {
125
+ await run();
126
+ }
127
+ catch (err) {
128
+ post({ type: 'error', error: errorMessage(err) });
129
+ }
130
+ }
131
+ /**
132
+ * Reject a message the host cannot act on. Requests with a dedicated
133
+ * error reply (init/switchProvider -> loadError, detect -> detectError)
134
+ * use it so legacy senders matching by type keep working; everything
135
+ * else gets the generic protocol error.
136
+ */
137
+ function rejectRequest(raw, problem) {
138
+ const msg = isRecord(raw) ? raw : {};
139
+ const requestId = optionalRequestId(msg.requestId);
140
+ const type = typeof msg.type === 'string' ? msg.type : undefined;
141
+ const error = type ? `Invalid ${type} message: ${problem}` : problem;
142
+ if (type === 'init' || type === 'switchProvider') {
143
+ post({ type: 'loadError', requestId, error });
144
+ }
145
+ else if (type === 'detect' && requestId !== undefined) {
146
+ post({ type: 'detectError', requestId, error });
147
+ }
148
+ else {
149
+ post({ type: 'error', requestId, error });
150
+ }
151
+ }
152
+ async function dispatch(msg) {
51
153
  switch (msg.type) {
52
154
  case 'init': {
53
155
  await handleLoad(msg.requestId, async () => {
@@ -87,30 +189,65 @@ export function serveEngine(engine, port) {
87
189
  break;
88
190
  }
89
191
  case 'setThreshold': {
90
- await engine.updateSettings({ threshold: msg.value });
192
+ await handleSetting(() => engine.updateSettings({ threshold: msg.value }));
91
193
  break;
92
194
  }
93
195
  case 'setCustomLabels': {
94
- await engine.updateSettings({ customLabels: msg.labels });
196
+ await handleSetting(() => engine.updateSettings({ customLabels: msg.labels }));
95
197
  break;
96
198
  }
97
199
  case 'setRegex': {
98
- const patch = { regexEnabled: msg.enabled };
99
- if (msg.region !== undefined)
100
- patch.regexRegion = msg.region;
101
- await engine.updateSettings(patch);
200
+ await handleSetting(async () => {
201
+ const patch = { regexEnabled: msg.enabled };
202
+ if (msg.region !== undefined)
203
+ patch.regexRegion = msg.region;
204
+ await engine.updateSettings(patch);
205
+ });
102
206
  break;
103
207
  }
104
208
  case 'setRegexRegion': {
105
- await engine.updateSettings({ regexRegion: msg.region });
209
+ await handleSetting(() => engine.updateSettings({ regexRegion: msg.region }));
106
210
  break;
107
211
  }
108
212
  case 'releaseModel': {
109
- await engine.release();
110
- post({ type: 'released', requestId: msg.requestId });
213
+ try {
214
+ await engine.release();
215
+ post({ type: 'released', requestId: msg.requestId });
216
+ }
217
+ catch (err) {
218
+ post({ type: 'error', requestId: msg.requestId, error: errorMessage(err) });
219
+ }
111
220
  break;
112
221
  }
113
222
  }
223
+ }
224
+ const unsubscribeMessages = port.onMessage(async (raw) => {
225
+ // R12: the port is same-origin but not trusted with the message shape.
226
+ // Anything that is not a well-formed request gets an error reply and
227
+ // never reaches the engine; nothing here may throw into the transport.
228
+ try {
229
+ if (!isRecord(raw) || typeof raw.type !== 'string') {
230
+ rejectRequest(raw, 'message must be an object with a string type');
231
+ return;
232
+ }
233
+ if (!REQUEST_TYPES.has(raw.type)) {
234
+ post({ type: 'error', requestId: optionalRequestId(raw.requestId), error: `Unknown message type: ${raw.type}` });
235
+ return;
236
+ }
237
+ const problem = requestProblem(raw);
238
+ if (problem) {
239
+ rejectRequest(raw, problem);
240
+ return;
241
+ }
242
+ await dispatch(raw);
243
+ }
244
+ catch (err) {
245
+ // Only reachable if posting a reply itself failed; report best-effort.
246
+ try {
247
+ post({ type: 'error', requestId: isRecord(raw) ? optionalRequestId(raw.requestId) : undefined, error: errorMessage(err) });
248
+ }
249
+ catch { /* transport gone */ }
250
+ }
114
251
  });
115
252
  return () => {
116
253
  unsubscribeMessages();
@@ -146,6 +283,10 @@ export function connectEngine(port, initial) {
146
283
  return entry;
147
284
  }
148
285
  const unsubscribe = port.onMessage((raw) => {
286
+ // Ignore anything that is not an object with a string type: the host is
287
+ // the only expected sender, but a malformed frame must not throw here.
288
+ if (!isRecord(raw) || typeof raw.type !== 'string')
289
+ return;
149
290
  const msg = raw;
150
291
  switch (msg.type) {
151
292
  case 'downloadProgress': {
@@ -182,6 +323,17 @@ export function connectEngine(port, initial) {
182
323
  takePending(msg.requestId)?.resolve(undefined);
183
324
  break;
184
325
  }
326
+ case 'error': {
327
+ const err = new Error(msg.error);
328
+ if (msg.fatal) {
329
+ // The host cannot serve any further request: fail everything
330
+ // that is still waiting instead of leaving promises hanging.
331
+ close(err);
332
+ break;
333
+ }
334
+ takePending(msg.requestId)?.reject(err);
335
+ break;
336
+ }
185
337
  }
186
338
  });
187
339
  function request(kind, message, onProgress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccloak/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Environment-agnostic document anonymization engine for the DocCloak ecosystem.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Witold Lojek",
package/rules/at.json CHANGED
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "id": "regex:at:postal",
38
38
  "entityType": "ADDRESS",
39
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
39
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
40
40
  "flags": "gu",
41
41
  "confidence": 0.8,
42
42
  "domains": [
package/rules/be.json CHANGED
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "id": "regex:be:postal",
38
38
  "entityType": "ADDRESS",
39
- "pattern": "\\b[1-9]\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
39
+ "pattern": "\\b[1-9]\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
40
40
  "flags": "gu",
41
41
  "confidence": 0.75,
42
42
  "domains": [
package/rules/ch.json CHANGED
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "id": "regex:ch:postal-city",
34
34
  "entityType": "ADDRESS",
35
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
35
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
36
36
  "flags": "gu",
37
37
  "confidence": 0.8,
38
38
  "domains": [
package/rules/de.json CHANGED
@@ -97,7 +97,7 @@
97
97
  {
98
98
  "id": "regex:de:postal",
99
99
  "entityType": "ADDRESS",
100
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
100
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
101
101
  "flags": "gu",
102
102
  "confidence": 0.8,
103
103
  "domains": [
package/rules/dk.json CHANGED
@@ -31,7 +31,7 @@
31
31
  {
32
32
  "id": "regex:dk:postal-city",
33
33
  "entityType": "ADDRESS",
34
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
34
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
35
35
  "flags": "gu",
36
36
  "confidence": 0.8,
37
37
  "domains": [
package/rules/es.json CHANGED
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "id": "regex:es:postal",
82
82
  "entityType": "ADDRESS",
83
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
83
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
84
84
  "flags": "gu",
85
85
  "confidence": 0.75,
86
86
  "domains": [
package/rules/fi.json CHANGED
@@ -31,7 +31,7 @@
31
31
  {
32
32
  "id": "regex:fi:postal-city",
33
33
  "entityType": "ADDRESS",
34
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
34
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
35
35
  "flags": "gu",
36
36
  "confidence": 0.8,
37
37
  "domains": [
package/rules/fr.json CHANGED
@@ -99,7 +99,7 @@
99
99
  {
100
100
  "id": "regex:fr:postal",
101
101
  "entityType": "ADDRESS",
102
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
102
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
103
103
  "flags": "gu",
104
104
  "confidence": 0.8,
105
105
  "domains": [
package/rules/it.json CHANGED
@@ -83,7 +83,7 @@
83
83
  {
84
84
  "id": "regex:it:postal",
85
85
  "entityType": "ADDRESS",
86
- "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
86
+ "pattern": "\\b\\d{5}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
87
87
  "flags": "gu",
88
88
  "confidence": 0.8,
89
89
  "domains": [
package/rules/no.json CHANGED
@@ -69,7 +69,7 @@
69
69
  {
70
70
  "id": "regex:no:postal",
71
71
  "entityType": "ADDRESS",
72
- "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
72
+ "pattern": "\\b\\d{4}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
73
73
  "flags": "gu",
74
74
  "confidence": 0.8,
75
75
  "domains": [
package/rules/pl.json CHANGED
@@ -116,7 +116,7 @@
116
116
  {
117
117
  "id": "regex:pl:postal",
118
118
  "entityType": "ADDRESS",
119
- "pattern": "\\b\\d{2}-\\d{3}\\s+[\\p{L}][\\p{L}\\s-]+\\b",
119
+ "pattern": "\\b\\d{2}-\\d{3}\\s+[\\p{L}][\\p{L}\\s-]*[\\p{L}](?![\\p{L}\\p{N}])",
120
120
  "flags": "gu",
121
121
  "confidence": 0.8,
122
122
  "domains": [
package/rules/pt.json CHANGED
@@ -50,7 +50,7 @@
50
50
  {
51
51
  "id": "regex:pt:postal",
52
52
  "entityType": "ADDRESS",
53
- "pattern": "\\b\\d{4}-\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]+\\b",
53
+ "pattern": "\\b\\d{4}-\\d{3}\\s+[\\p{L}][\\p{L}\\s'-]*[\\p{L}](?![\\p{L}\\p{N}])",
54
54
  "flags": "gu",
55
55
  "confidence": 0.85,
56
56
  "domains": [