@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.11.31
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 +530 -0
- package/LICENSE +24 -0
- package/README.md +184 -2
- package/bin/legacy-example-package-name.js +95 -0
- package/bin/sensitive-data-sanitizer.js +430 -0
- package/bin/stream.js +56 -0
- package/docs/API.md +336 -0
- package/docs/COVERAGE.md +136 -0
- package/docs/HISTORY-REMEDIATION.md +41 -0
- package/examples/combined-scanners.mjs +23 -0
- package/examples/presidio-bridge.py +32 -0
- package/examples/realistic-fakes.mjs +18 -0
- package/examples/sanitize-session.mjs +27 -0
- package/examples/shared-tool-logs.mjs +21 -0
- package/examples/stream-jsonl.mjs +17 -0
- package/package.json +124 -4
- package/src/adapters.js +204 -0
- package/src/arithmetic.js +11 -0
- package/src/confidence.js +65 -0
- package/src/context.js +424 -0
- package/src/credentials.js +54 -0
- package/src/detection.js +293 -0
- package/src/encoded.js +233 -0
- package/src/engines.js +348 -0
- package/src/entities.js +166 -0
- package/src/escape-spans.js +20 -0
- package/src/eslint-plugin.d.ts +10 -0
- package/src/eslint-plugin.js +193 -0
- package/src/external.js +169 -0
- package/src/fake-names.js +222 -0
- package/src/fake.js +524 -0
- package/src/files.js +131 -0
- package/src/gitleaks.js +194 -0
- package/src/history-rewrite.js +339 -0
- package/src/history.js +103 -0
- package/src/identity.js +274 -0
- package/src/index.d.ts +321 -0
- package/src/index.js +40 -0
- package/src/jsonl-stream.js +198 -0
- package/src/keywords.js +45 -0
- package/src/known.js +164 -0
- package/src/legacy.d.ts +3 -0
- package/src/legacy.js +2 -0
- package/src/locales.js +217 -0
- package/src/log-bytes.js +85 -0
- package/src/names.js +65 -0
- package/src/national-checks.js +129 -0
- package/src/national.js +128 -0
- package/src/outbound.js +28 -0
- package/src/personal-context.js +85 -0
- package/src/personal.js +248 -0
- package/src/projection.js +126 -0
- package/src/propagation.js +163 -0
- package/src/public-verifier.js +77 -0
- package/src/public.js +153 -0
- package/src/record-worker.js +20 -0
- package/src/rules.js +210 -0
- package/src/sanitizer.js +794 -0
- package/src/specimens.js +34 -0
- package/src/stream-identity.js +129 -0
- package/src/stream-worker.js +18 -0
- package/src/stream.js +449 -0
- package/src/structured.js +278 -0
- package/src/transforms.js +192 -0
- package/src/transliteration.js +75 -0
- package/src/utf8.js +60 -0
- package/src/vendor/entities/aws.json +42 -0
- package/src/vendor/entities/azure.json +182 -0
- package/src/vendor/entities/google.json +219 -0
- package/src/vendor/gitleaks/LICENSE +21 -0
- package/src/vendor/gitleaks/provenance.json +8 -0
- package/src/vendor/gitleaks/rules-0.json +268 -0
- package/src/vendor/gitleaks/rules-1.json +1794 -0
- package/src/vendor/gitleaks/rules-2.json +292 -0
- package/src/vendor/gitleaks/rules-3.json +263 -0
- package/src/vendor/gitleaks/rules-4.json +277 -0
- package/src/vendor/gitleaks/rules-5.json +154 -0
- package/src/worker-options.js +27 -0
package/src/public.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
|
|
3
|
+
// Exact public knowledge and narrowly scoped role-mail heuristics. A role name
|
|
4
|
+
// alone is never an exemption on an unknown/personal domain. Credentials and
|
|
5
|
+
// caller-specified private values always outrank these automatic exceptions.
|
|
6
|
+
export const publicKnowledge = {
|
|
7
|
+
PERSON: [
|
|
8
|
+
['Albert Einstein', 'Q937'],
|
|
9
|
+
['Marie Curie', 'Q7186'],
|
|
10
|
+
['Isaac Newton', 'Q935'],
|
|
11
|
+
['Charles Darwin', 'Q1035'],
|
|
12
|
+
['Alan Turing', 'Q7251'],
|
|
13
|
+
['Ada Lovelace', 'Q7259'],
|
|
14
|
+
['Bill Gates', 'Q5284'],
|
|
15
|
+
['Elon Musk', 'Q317521'],
|
|
16
|
+
['Linus Torvalds', 'Q34253'],
|
|
17
|
+
['Satya Nadella', 'Q7426870'],
|
|
18
|
+
['Tim Cook', 'Q265852'],
|
|
19
|
+
['Grace Hopper', 'Q11641'],
|
|
20
|
+
['Sergey Brin', 'Q92764'],
|
|
21
|
+
['Pavel Durov', 'Q149067'],
|
|
22
|
+
['Vladimir Putin', 'Q7747'],
|
|
23
|
+
['Barack Obama', 'Q76'],
|
|
24
|
+
['Nelson Mandela', 'Q8023'],
|
|
25
|
+
['Mahatma Gandhi', 'Q1001'],
|
|
26
|
+
['Martin Luther King', 'Q8027'],
|
|
27
|
+
['Альберт Эйнштейн', 'Q937'],
|
|
28
|
+
['Мария Кюри', 'Q7186'],
|
|
29
|
+
['Исаак Ньютон', 'Q935'],
|
|
30
|
+
['李白', 'Q7071'],
|
|
31
|
+
['夏目漱石', 'Q48443'],
|
|
32
|
+
].map(([value, id]) => ({
|
|
33
|
+
value,
|
|
34
|
+
source: `https://www.wikidata.org/wiki/${id}`,
|
|
35
|
+
})),
|
|
36
|
+
ORGANIZATION: [
|
|
37
|
+
['Microsoft', 'Q2283'],
|
|
38
|
+
['Google', 'Q95'],
|
|
39
|
+
['Apple', 'Q312'],
|
|
40
|
+
['GitHub', 'Q364'],
|
|
41
|
+
['Wikimedia Foundation', 'Q180'],
|
|
42
|
+
['United Nations', 'Q1065'],
|
|
43
|
+
['NASA', 'Q23548'],
|
|
44
|
+
['UNESCO', 'Q7809'],
|
|
45
|
+
].map(([value, id]) => ({
|
|
46
|
+
value,
|
|
47
|
+
source: `https://www.wikidata.org/wiki/${id}`,
|
|
48
|
+
})),
|
|
49
|
+
IP_ADDRESS: [
|
|
50
|
+
...[
|
|
51
|
+
'8.8.8.8',
|
|
52
|
+
'8.8.4.4',
|
|
53
|
+
'2001:4860:4860::8888',
|
|
54
|
+
'2001:4860:4860::8844',
|
|
55
|
+
].map((value) => ({
|
|
56
|
+
value,
|
|
57
|
+
source: 'https://developers.google.com/speed/public-dns/docs/using',
|
|
58
|
+
})),
|
|
59
|
+
...[
|
|
60
|
+
'1.1.1.1',
|
|
61
|
+
'1.0.0.1',
|
|
62
|
+
'2606:4700:4700::1111',
|
|
63
|
+
'2606:4700:4700::1001',
|
|
64
|
+
].map((value) => ({
|
|
65
|
+
value,
|
|
66
|
+
source: 'https://developers.cloudflare.com/1.1.1.1/ip-addresses/',
|
|
67
|
+
})),
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
export const organizationDomains = new Set([
|
|
71
|
+
'microsoft.com',
|
|
72
|
+
'google.com',
|
|
73
|
+
'apple.com',
|
|
74
|
+
'github.com',
|
|
75
|
+
'wikimedia.org',
|
|
76
|
+
'wikipedia.org',
|
|
77
|
+
'mozilla.org',
|
|
78
|
+
'cloudflare.com',
|
|
79
|
+
]);
|
|
80
|
+
export const serviceAccounts = new Set([
|
|
81
|
+
'runner',
|
|
82
|
+
'ubuntu',
|
|
83
|
+
'vsts',
|
|
84
|
+
'circleci',
|
|
85
|
+
'node',
|
|
86
|
+
'root',
|
|
87
|
+
]);
|
|
88
|
+
export function servicePath(value) {
|
|
89
|
+
const account = /^\/home\/([^/]+)\//.exec(value)?.[1];
|
|
90
|
+
return (
|
|
91
|
+
serviceAccounts.has(account) ||
|
|
92
|
+
/^\/github\/(?:workspace|runner_temp)\//.test(value)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const roles =
|
|
96
|
+
/^(?:press|media|support|info|contact|security|abuse|privacy|sales|noreply|no-reply)$/i;
|
|
97
|
+
|
|
98
|
+
export function isPublic(text, finding, options) {
|
|
99
|
+
if (
|
|
100
|
+
options.publicKnowledge === false ||
|
|
101
|
+
finding.category === 'credential' ||
|
|
102
|
+
['known-personal', 'confirmed-personal'].includes(finding.rule)
|
|
103
|
+
) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const value = text.slice(finding.start, finding.end).normalize('NFC');
|
|
107
|
+
// Sensitive relationship context wins over a common public/private namesake.
|
|
108
|
+
if (
|
|
109
|
+
finding.type === 'PERSON' &&
|
|
110
|
+
/(?:patient|customer|employee|my name|пациент|клиент)\s*[:=]?\s*$/iu.test(
|
|
111
|
+
text.slice(Math.max(0, finding.start - 64), finding.start)
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
(publicKnowledge[finding.type] ?? []).some((entry) => entry.value === value)
|
|
118
|
+
) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (finding.type === 'EMAIL') {
|
|
122
|
+
const [local, domain] = value.toLowerCase().split('@');
|
|
123
|
+
return roles.test(local) && organizationDomains.has(domain);
|
|
124
|
+
}
|
|
125
|
+
if (finding.type === 'IP_ADDRESS') {
|
|
126
|
+
return (
|
|
127
|
+
localAddress(value) ||
|
|
128
|
+
/^(?:192\.0\.2\.|198\.51\.100\.|203\.0\.113\.)\d{1,3}$/.test(value) ||
|
|
129
|
+
/^2001:db8:/i.test(value)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function localAddress(value) {
|
|
136
|
+
if (isIP(value) === 6) {
|
|
137
|
+
return (
|
|
138
|
+
/^(?:::1|0:0:0:0:0:0:0:1)$/i.test(value) ||
|
|
139
|
+
/^(?:f[cd][\da-f]{2}:|fe[89ab][\da-f]:)/i.test(value)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (isIP(value) !== 4) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const [a, b] = value.split('.').map(Number);
|
|
146
|
+
return (
|
|
147
|
+
a === 127 ||
|
|
148
|
+
a === 10 ||
|
|
149
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
150
|
+
(a === 192 && b === 168) ||
|
|
151
|
+
(a === 169 && b === 254)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { createSanitizer, prepareStreamEngine } from './engines.js';
|
|
3
|
+
|
|
4
|
+
parentPort.on(
|
|
5
|
+
'message',
|
|
6
|
+
async ({ text, confirmedPersonal, nativeFindings }) => {
|
|
7
|
+
try {
|
|
8
|
+
const sanitizer = createSanitizer({ ...workerData, confirmedPersonal });
|
|
9
|
+
await prepareStreamEngine(sanitizer, text, nativeFindings);
|
|
10
|
+
parentPort.postMessage({ value: (await sanitizer.sanitize(text)).text });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
parentPort.postMessage({
|
|
13
|
+
error: /^ERR_[A-Z_]+$/.test(error.code) ? error.code : 'ERR_WORKER',
|
|
14
|
+
record: error.record,
|
|
15
|
+
offset: error.offset,
|
|
16
|
+
recordOffset: error.recordOffset,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
);
|
package/src/rules.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { localeVocabulary, personalFieldTypes } from './locales.js';
|
|
2
|
+
|
|
3
|
+
// Supplemental formats, independently maintained from upstream Secretlint.
|
|
4
|
+
// Lengths are intentionally conservative: suspicious prefixes are redacted
|
|
5
|
+
// even when a pasted credential is truncated. No online verification occurs.
|
|
6
|
+
export const serviceRules = [
|
|
7
|
+
['gitlab-runner', /GR1348941[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/g],
|
|
8
|
+
[
|
|
9
|
+
'firebase',
|
|
10
|
+
/(?<![A-Za-z0-9_-])AAAA[A-Za-z0-9_-]{6}[A-Za-z0-9_-]*:APA91b[A-Za-z0-9_-]{32}[A-Za-z0-9_-]*/g,
|
|
11
|
+
],
|
|
12
|
+
['github', /(?:github_pat_|gh[pousr]_)[A-Za-z0-9_]{16}[A-Za-z0-9_]*/g],
|
|
13
|
+
[
|
|
14
|
+
'gitlab',
|
|
15
|
+
/(?:glpat|gloas|gldt|glrt|glrtr|glcbt|glptt|glft|glimt|glagent|glwt|glsoat|glffct)-[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/g,
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
'openai',
|
|
19
|
+
/(?<![A-Za-z0-9_])sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/g,
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
'anthropic',
|
|
23
|
+
/(?<![A-Za-z0-9_])sk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/g,
|
|
24
|
+
],
|
|
25
|
+
['aws', /(?:AKIA|ASIA|A3T[A-Z0-9])[A-Z0-9]{16}/g],
|
|
26
|
+
[
|
|
27
|
+
'google',
|
|
28
|
+
/(?:AIza[A-Za-z0-9_-]{35}|GOCSPX-[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*|ya29\.[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*)/g,
|
|
29
|
+
],
|
|
30
|
+
['slack', /(?:xox[a-z]|xapp|xwfp)-[A-Za-z0-9-]{16}[A-Za-z0-9-]*/g],
|
|
31
|
+
[
|
|
32
|
+
'stripe',
|
|
33
|
+
/(?:[sr]k_(?:live|test)_[A-Za-z0-9]{16}[A-Za-z0-9]*|whsec_[A-Za-z0-9]{16}[A-Za-z0-9]*)/g,
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'sendgrid',
|
|
37
|
+
/SG\.[A-Za-z0-9_-]{10}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g,
|
|
38
|
+
],
|
|
39
|
+
['twilio', /SK[a-fA-F0-9]{32}/g],
|
|
40
|
+
['npm', /npm_[A-Za-z0-9]{20}[A-Za-z0-9]*/g],
|
|
41
|
+
['pypi', /pypi-[A-Za-z0-9_-]{30}[A-Za-z0-9_-]*/g],
|
|
42
|
+
['telegram', /\d{8,12}:[A-Za-z0-9_-]{30}[A-Za-z0-9_-]*/g],
|
|
43
|
+
[
|
|
44
|
+
'discord',
|
|
45
|
+
/[MN][A-Za-z0-9_-]{23}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g,
|
|
46
|
+
],
|
|
47
|
+
['huggingface', /hf_[A-Za-z0-9]{20}[A-Za-z0-9]*/g],
|
|
48
|
+
['shopify', /shp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}/g],
|
|
49
|
+
['databricks', /dapi[a-fA-F0-9]{32}/g],
|
|
50
|
+
['square', /sq0(?:atp|csp)-[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g],
|
|
51
|
+
['mailchimp', /(?<![a-fA-F0-9])[a-fA-F0-9]{32}-us\d{1,2}/g],
|
|
52
|
+
['digitalocean', /do(?:pat|o|r)_v1_[a-fA-F0-9]{64}/g],
|
|
53
|
+
['vault', /(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{16}[A-Za-z0-9_-]*/g],
|
|
54
|
+
['grafana', /(?:glc_|glsa_)[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g],
|
|
55
|
+
['linear', /lin_api_[A-Za-z0-9]{20}[A-Za-z0-9]*/g],
|
|
56
|
+
['notion', /(?:secret_|ntn_)[A-Za-z0-9]{32}[A-Za-z0-9]*/g],
|
|
57
|
+
['vercel', /vercel_[A-Za-z0-9_-]{20}[A-Za-z0-9_-]*/g],
|
|
58
|
+
[
|
|
59
|
+
'jwt',
|
|
60
|
+
/eyJ[A-Za-z0-9_-]{8}[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]{8}[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{8}[A-Za-z0-9_-]*/g,
|
|
61
|
+
],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Label recognition is deliberately separate from general free-text NER.
|
|
65
|
+
export const labels = {
|
|
66
|
+
credential: [
|
|
67
|
+
'password',
|
|
68
|
+
'passwd',
|
|
69
|
+
'pwd',
|
|
70
|
+
'passphrase',
|
|
71
|
+
'credentials',
|
|
72
|
+
'api key',
|
|
73
|
+
'mot de passe',
|
|
74
|
+
'kata sandi',
|
|
75
|
+
'mật khẩu',
|
|
76
|
+
'пароля',
|
|
77
|
+
'паролем',
|
|
78
|
+
'паролю',
|
|
79
|
+
'contraseñas',
|
|
80
|
+
'senhas',
|
|
81
|
+
'şifresi',
|
|
82
|
+
'पासवर्ड है',
|
|
83
|
+
'secret',
|
|
84
|
+
'token',
|
|
85
|
+
'api_key',
|
|
86
|
+
'apikey',
|
|
87
|
+
'api-key',
|
|
88
|
+
'authorization',
|
|
89
|
+
'auth',
|
|
90
|
+
'cookie',
|
|
91
|
+
'accountkey',
|
|
92
|
+
'client_secret',
|
|
93
|
+
'private_key',
|
|
94
|
+
'access_key',
|
|
95
|
+
'access_token',
|
|
96
|
+
'refresh_token',
|
|
97
|
+
'session_key',
|
|
98
|
+
'connectionstring',
|
|
99
|
+
'sas',
|
|
100
|
+
'sig',
|
|
101
|
+
'senha',
|
|
102
|
+
'contraseña',
|
|
103
|
+
'clave',
|
|
104
|
+
'mot_de_passe',
|
|
105
|
+
'passwort',
|
|
106
|
+
'пароль',
|
|
107
|
+
'секрет',
|
|
108
|
+
'токен',
|
|
109
|
+
'密码',
|
|
110
|
+
'密碼',
|
|
111
|
+
'口令',
|
|
112
|
+
'パスワード',
|
|
113
|
+
'비밀번호',
|
|
114
|
+
'كلمة المرور',
|
|
115
|
+
'رمز الدخول',
|
|
116
|
+
'סיסמה',
|
|
117
|
+
'पासवर्ड',
|
|
118
|
+
'şifre',
|
|
119
|
+
'kata_sandi',
|
|
120
|
+
'mật_khẩu',
|
|
121
|
+
'รหัสผ่าน',
|
|
122
|
+
],
|
|
123
|
+
PERSON: [
|
|
124
|
+
'Applicant',
|
|
125
|
+
'Passenger',
|
|
126
|
+
'Traveller',
|
|
127
|
+
'Заявитель',
|
|
128
|
+
'name',
|
|
129
|
+
'full_name',
|
|
130
|
+
'fullname',
|
|
131
|
+
'first_name',
|
|
132
|
+
'last_name',
|
|
133
|
+
'givenname',
|
|
134
|
+
'surname',
|
|
135
|
+
'nom',
|
|
136
|
+
'nombre',
|
|
137
|
+
'nome',
|
|
138
|
+
'имя',
|
|
139
|
+
'фио',
|
|
140
|
+
'фамилия',
|
|
141
|
+
'姓名',
|
|
142
|
+
'氏名',
|
|
143
|
+
'이름',
|
|
144
|
+
'الاسم',
|
|
145
|
+
'שם',
|
|
146
|
+
'नाम',
|
|
147
|
+
'ad_soyad',
|
|
148
|
+
'nama',
|
|
149
|
+
'họ_tên',
|
|
150
|
+
'ชื่อ',
|
|
151
|
+
],
|
|
152
|
+
ADDRESS: [
|
|
153
|
+
'address',
|
|
154
|
+
'street_address',
|
|
155
|
+
'adresse',
|
|
156
|
+
'dirección',
|
|
157
|
+
'endereço',
|
|
158
|
+
'адрес',
|
|
159
|
+
'地址',
|
|
160
|
+
'住所',
|
|
161
|
+
'주소',
|
|
162
|
+
'العنوان',
|
|
163
|
+
'כתובת',
|
|
164
|
+
'पता',
|
|
165
|
+
'adres',
|
|
166
|
+
'alamat',
|
|
167
|
+
'địa_chỉ',
|
|
168
|
+
],
|
|
169
|
+
DATE_OF_BIRTH: [
|
|
170
|
+
'date_of_birth',
|
|
171
|
+
'dob',
|
|
172
|
+
'birthdate',
|
|
173
|
+
'fecha_de_nacimiento',
|
|
174
|
+
'дата рождения',
|
|
175
|
+
'出生日期',
|
|
176
|
+
'生年月日',
|
|
177
|
+
'تاريخ الميلاد',
|
|
178
|
+
],
|
|
179
|
+
ID: [
|
|
180
|
+
'passport',
|
|
181
|
+
'passport_number',
|
|
182
|
+
'ssn',
|
|
183
|
+
'national_id',
|
|
184
|
+
'tax_id',
|
|
185
|
+
'inn',
|
|
186
|
+
'инн',
|
|
187
|
+
'social_security',
|
|
188
|
+
'cpf',
|
|
189
|
+
'aadhaar',
|
|
190
|
+
'snils',
|
|
191
|
+
'паспорт',
|
|
192
|
+
'снилс',
|
|
193
|
+
'инн',
|
|
194
|
+
'身份证',
|
|
195
|
+
'护照',
|
|
196
|
+
'身分証',
|
|
197
|
+
'جواز السفر',
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
for (const [, credential, ...fields] of localeVocabulary) {
|
|
202
|
+
labels.credential.push(credential);
|
|
203
|
+
fields.forEach((word, i) => {
|
|
204
|
+
labels[personalFieldTypes[i]] ??= [];
|
|
205
|
+
labels[personalFieldTypes[i]].push(word);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
for (const [type, words] of Object.entries(labels)) {
|
|
209
|
+
labels[type] = [...new Set(words)].sort((a, b) => b.length - a.length);
|
|
210
|
+
}
|