fieldshield 1.0.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.
@@ -0,0 +1,790 @@
1
+ import { jsxs as D, jsx as d, Fragment as le } from "react/jsx-runtime";
2
+ import { useState as G, useRef as M, useEffect as ee, useCallback as z, forwardRef as he, useId as ge, useImperativeHandle as me } from "react";
3
+ const te = Object.freeze({
4
+ // ── AI / Cloud credentials ────────────────────────────────────────────────
5
+ /**
6
+ * OpenAI keys (all generations) and Anthropic keys — all use `sk-` prefix.
7
+ *
8
+ * - Old OpenAI personal keys: `sk-[alphanum]{20+}`
9
+ * - New OpenAI project keys: `sk-proj-[alphanum+hyphen]{20+}`
10
+ * - New OpenAI service account: `sk-svcacct-[alphanum+hyphen]{20+}`
11
+ * - Anthropic keys: `sk-ant-api03-[alphanum+hyphen]{20+}`
12
+ *
13
+ * The updated pattern allows hyphens in the key body (`[a-zA-Z0-9-]`) so
14
+ * all `sk-*` variants are caught by a single alternative. The `ant-api-`
15
+ * alternative is retained for any legacy keys not using the `sk-` prefix.
16
+ * Google AIza keys remain unchanged.
17
+ */
18
+ AI_API_KEY: "(sk-[a-zA-Z0-9][a-zA-Z0-9-]{19,}|ant-api-[a-z0-9-]{20,}|AIza[0-9A-Za-z_-]{35})",
19
+ /**
20
+ * AWS permanent (`AKIA`) and temporary (`ASIA`) access key prefixes.
21
+ * Fixed: previous pattern had `ASXA` which is not a real AWS prefix —
22
+ * the correct temporary credential prefix is `ASIA`.
23
+ * 16-character alphanumeric uppercase suffix after the 4-char prefix.
24
+ */
25
+ AWS_ACCESS_KEY: "\\b(AKIA|ASIA)[0-9A-Z]{16}\\b",
26
+ // ── PII ───────────────────────────────────────────────────────────────────
27
+ /**
28
+ * US Social Security Number — all common separator formats.
29
+ *
30
+ * Matches:
31
+ * `123-45-6789` hyphen separated (standard printed format)
32
+ * `123 45 6789` space separated (typed on mobile without hyphen key)
33
+ * `123.45.6789` dot separated (common in some form UIs)
34
+ * `123456789` no separator (common when copy-pasting from a database)
35
+ *
36
+ * `[-\s.]?` makes the separator optional, catching all four formats
37
+ * with a single pattern. False positive risk on bare 9-digit numbers is
38
+ * acceptable in a security context — a missed SSN is worse than a
39
+ * false positive that briefly highlights a non-sensitive number.
40
+ *
41
+ * @remarks **False positive risk (medium):**
42
+ * The bare 9-digit form (`123456789`) matches any 9-digit number. This
43
+ * overlaps with `TAX_ID` bare form and with arbitrary 9-digit numeric IDs.
44
+ * Both `SSN` and `TAX_ID` firing on the same value is intentional — the
45
+ * field may contain either, and consuming applications can use field context
46
+ * to disambiguate. The hyphenated form (`123-45-6789`) has very low false
47
+ * positive risk and is the recommended format to require in dedicated SSN
48
+ * fields.
49
+ */
50
+ SSN: "\\b\\d{3}[-\\s.]?\\d{2}[-\\s.]?\\d{4}\\b",
51
+ /**
52
+ * RFC 5321-compatible email address.
53
+ * Covers standard alphanumeric local parts with common special characters.
54
+ */
55
+ EMAIL: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
56
+ /**
57
+ * US phone numbers — relaxed exchange constraint to catch real-world input.
58
+ *
59
+ * Previous pattern required exchange to start with [2-9] (NANP-strict),
60
+ * which rejected common inputs like `555-123-4567` where the exchange
61
+ * starts with 1. Relaxed to `\d{3}` since FieldShield is a security
62
+ * library, not a phone number validator — false negatives are worse
63
+ * than false positives here.
64
+ *
65
+ * Also adds an international format alternative (`\+[1-9]\d{6,14}`)
66
+ * for non-US numbers like `+44 7911 123456`.
67
+ *
68
+ * Matches:
69
+ * `555-123-4567` US hyphen
70
+ * `555 123 4567` US space
71
+ * `5551234567` US no separator
72
+ * `(555) 123-4567` US with area code parens
73
+ * `+1 555 123 4567` US with country code
74
+ * `+44 7911 123456` UK mobile
75
+ * `+91 98765 43210` India mobile
76
+ */
77
+ PHONE: "\\b(?:\\+?1[-. ]?)?\\(?[2-9]\\d{2}\\)?[-. ]?\\d{3}[-. ]?\\d{4}\\b|\\+[1-9][\\s.-]?(?:\\d[\\s.-]?){6,14}\\d\\b",
78
+ // ── Financial ────────────────────────────────────────────────────────────
79
+ //
80
+ // IBAN was moved to OPT_IN_PATTERNS — see that export for the rationale.
81
+ /**
82
+ * Visa, Mastercard, and American Express — with optional space or hyphen
83
+ * separators between digit groups.
84
+ *
85
+ * Previous pattern required consecutive digits, missing the most common
86
+ * real-world format (`4111 1111 1111 1111`) users type or paste from cards.
87
+ *
88
+ * Matches:
89
+ * Visa 16-digit: `4111111111111111` / `4111 1111 1111 1111` / `4111-1111-1111-1111`
90
+ * Mastercard: `5500005555555559` / `5500 0055 5555 5559`
91
+ * Amex 15-digit: `378282246310005` / `3782 822463 10005`
92
+ *
93
+ * Does not run a Luhn checksum — add post-match validation in production
94
+ * to reduce false positives on structurally-matching non-card numbers.
95
+ */
96
+ CREDIT_CARD: [
97
+ "\\b4\\d{3}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b",
98
+ "\\b5[1-5]\\d{2}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b",
99
+ "\\b3[47]\\d{2}[-\\s]?\\d{6}[-\\s]?\\d{5}\\b"
100
+ ].join("|"),
101
+ /**
102
+ * Date of birth — common formats used in healthcare and fintech forms.
103
+ *
104
+ * Matches:
105
+ * `MM/DD/YYYY` US slash format e.g. `01/15/1990`
106
+ * `MM-DD-YYYY` US hyphen format e.g. `01-15-1990`
107
+ * `MM.DD.YYYY` US dot format e.g. `01.15.1990`
108
+ * `YYYY-MM-DD` ISO 8601 e.g. `1990-01-15`
109
+ * `YYYY/MM/DD` ISO with slashes e.g. `1990/01/15`
110
+ *
111
+ * Year range constrained to 1900–2099 to avoid matching arbitrary
112
+ * date-like numbers. Month constrained to 01–12, day to 01–31.
113
+ *
114
+ * @remarks **False positive risk (medium — acceptable in healthcare contexts):**
115
+ * Any date in a clinical note will trigger this pattern. This is intentional
116
+ * because dates are PHI under HIPAA (45 CFR §164.514(b)(2)(i) lists dates as
117
+ * one of the 18 PHI identifiers). Consuming applications should be aware that
118
+ * date-heavy fields (e.g. encounter notes, lab reports) will fire frequently.
119
+ * Consider `a11yMode` or field-level pattern overrides for fields where dates
120
+ * are expected but not individually sensitive.
121
+ */
122
+ DATE_OF_BIRTH: "\\b(?:0?[1-9]|1[0-2])[-/.](?:0?[1-9]|[12]\\d|3[01])[-/.](?:19|20)\\d{2}\\b|\\b(?:19|20)\\d{2}[-/.](?:0?[1-9]|1[0-2])[-/.](?:0?[1-9]|[12]\\d|3[01])\\b",
123
+ /**
124
+ * US Employer Identification Number (EIN) / Tax ID.
125
+ *
126
+ * Matches:
127
+ * `12-3456789` hyphenated EIN format
128
+ * `123456789` 9-digit no separator (also overlaps with SSN bare digits)
129
+ *
130
+ * @remarks **False positive risk (medium):**
131
+ * The bare 9-digit form (`123456789`) matches any 9-digit number — zip codes,
132
+ * product IDs, reference numbers, and other identifiers all have 9 digits.
133
+ * Use this pattern on tax-specific fields (W-9, EIN entry) rather than
134
+ * general free-text fields to avoid excessive false positives. Both `SSN` and
135
+ * `TAX_ID` firing on the same bare 9-digit value is intentional — the field
136
+ * may contain either, and consuming applications can use field context to
137
+ * disambiguate.
138
+ */
139
+ TAX_ID: "\\b\\d{2}-\\d{7}\\b|\\b\\d{9}\\b",
140
+ // ── Credentials (developer / admin tooling) ───────────────────────────────
141
+ //
142
+ // These patterns are unlikely to appear in typical end-user consumer forms.
143
+ // They are included for applications that expose developer-facing inputs —
144
+ // config panels, support chat, API key management UIs — where accidental
145
+ // credential exposure is a real risk.
146
+ //
147
+ // Consumer-facing deployments can safely ignore these — a user entering
148
+ // their SSN will never trigger GITHUB_TOKEN or JWT.
149
+ /**
150
+ * GitHub personal access tokens and fine-grained PATs.
151
+ *
152
+ * Prefixes:
153
+ * `ghp_` classic personal access token
154
+ * `gho_` OAuth app token
155
+ * `ghs_` GitHub Apps server-to-server token
156
+ * `ghu_` GitHub Apps user-to-server token
157
+ * `github_pat_` fine-grained personal access token (newer format)
158
+ */
159
+ GITHUB_TOKEN: "\\b(ghp|gho|ghs|ghu|github_pat)_[a-zA-Z0-9_]{20,}\\b",
160
+ /**
161
+ * Stripe API keys — secret, publishable, and restricted.
162
+ *
163
+ * Prefixes:
164
+ * `sk_live_` / `sk_test_` secret keys (highest privilege — never expose)
165
+ * `pk_live_` / `pk_test_` publishable keys (client-safe but worth flagging)
166
+ * `rk_live_` / `rk_test_` restricted keys
167
+ *
168
+ * `sk_live_` keys are the highest-value target — a leaked live secret key
169
+ * gives full Stripe account access.
170
+ */
171
+ STRIPE_KEY: "\\b(sk|pk|rk)_(live|test)_[a-zA-Z0-9]{20,}\\b",
172
+ /**
173
+ * JSON Web Token (JWT) — three base64url segments separated by dots.
174
+ *
175
+ * All JWTs begin with `eyJ` (base64url encoding of `{"`) making them
176
+ * highly detectable with very low false positive rate. JWTs appear in
177
+ * support tickets, config forms, and debug paste fields constantly —
178
+ * a valid JWT pasted anywhere is a potential session hijack.
179
+ */
180
+ JWT: "\\beyJ[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\b",
181
+ /**
182
+ * PEM private key block header.
183
+ *
184
+ * Matches the opening line of RSA, EC, and OpenSSH private keys.
185
+ * The header alone is sufficient to flag the paste — the base64 body
186
+ * that follows does not need to match.
187
+ *
188
+ * Covers:
189
+ * `-----BEGIN PRIVATE KEY-----`
190
+ * `-----BEGIN RSA PRIVATE KEY-----`
191
+ * `-----BEGIN EC PRIVATE KEY-----`
192
+ * `-----BEGIN OPENSSH PRIVATE KEY-----`
193
+ *
194
+ * Does NOT match `-----BEGIN PUBLIC KEY-----` or `-----BEGIN CERTIFICATE-----`
195
+ * which are not sensitive.
196
+ */
197
+ PRIVATE_KEY_BLOCK: "-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
198
+ // ── Healthcare identifiers ────────────────────────────────────────────────
199
+ //
200
+ // UK_NIN is the sole built-in healthcare identifier. DEA_NUMBER and
201
+ // NPI_NUMBER were moved to OPT_IN_PATTERNS — see that export for the
202
+ // rationale on each.
203
+ // ── International PII ─────────────────────────────────────────────────────
204
+ /**
205
+ * UK National Insurance Number (NIN) — equivalent of US SSN for UK residents.
206
+ *
207
+ * Format: 2 letters + 6 digits + 1 letter suffix, optionally space-separated.
208
+ * `AB 12 34 56 C` standard printed format (spaces between pairs)
209
+ * `AB123456C` compact format (no spaces)
210
+ *
211
+ * Constraints:
212
+ * - First letter: not D, F, I, Q, U, V; not BG, GB, KN, NK, NT, TN, ZZ
213
+ * - Second letter: not D, F, I, Q, U, V
214
+ * - Suffix: A, B, C, or D only
215
+ *
216
+ * Pattern simplifies the first/second letter exclusions to the most common
217
+ * valid range — captures all real NINs while excluding the most common
218
+ * invalid prefixes.
219
+ *
220
+ * Matches:
221
+ * `AB 12 34 56 C` standard spaced format
222
+ * `AB123456C` compact no-space format
223
+ * `QQ 12 34 56 A` valid prefix
224
+ */
225
+ UK_NIN: "\\b[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]\\s?\\d{2}\\s?\\d{2}\\s?\\d{2}\\s?[A-D]\\b"
226
+ }), ye = Object.freeze({
227
+ /**
228
+ * International Bank Account Number (IBAN) — with or without spaces.
229
+ *
230
+ * **Why opt-in:**
231
+ * The pattern opens with two uppercase letters followed by two digits, then
232
+ * continues with alphanumeric groups. In clinical and pharmaceutical contexts,
233
+ * structured identifiers that share this shape — laboratory accession numbers,
234
+ * specimen container IDs, reagent lot codes, GS1 product codes, and ISO
235
+ * country-code-prefixed reference IDs — trigger this pattern. IBAN detection
236
+ * is only meaningful on dedicated payment or banking fields; enabling it on
237
+ * healthcare or general free-text fields produces frequent false positives.
238
+ *
239
+ * **When to use:** Payment instruction fields, wire transfer forms, banking
240
+ * account entry — fields where an IBAN is the specifically expected value.
241
+ *
242
+ * Matches:
243
+ * `GB82WEST12345698765432` no spaces
244
+ * `GB82 WEST 1234 5698 7654 32` standard printed format
245
+ * `DE89370400440532013000` German IBAN no spaces
246
+ */
247
+ IBAN: "\\b[A-Z]{2}\\d{2}[-\\s]?(?:[A-Z0-9]{1,4}[-\\s]?){3,}[A-Z0-9]{1,4}\\b",
248
+ /**
249
+ * DEA Registration Number — US Drug Enforcement Administration prescriber ID.
250
+ *
251
+ * Format: 2 letters + 7 digits.
252
+ * - First letter: registrant type code (A-Z minus I, N, O, V, W, Y, Z)
253
+ * - Second letter: first letter of registrant's last name (A-Z)
254
+ * - 7 digits: unique identifier with check digit
255
+ *
256
+ * **Why opt-in:**
257
+ * The pattern `[A-Z]{2}\d{7}` matches any two uppercase letters followed by
258
+ * seven digits. This shape is ubiquitous in pharmaceutical and clinical contexts:
259
+ * medication lot numbers (`AB1234567`), product batch codes (`CD9876543`),
260
+ * laboratory reagent IDs, and equipment serial numbers all satisfy the constraint.
261
+ * In clinical notes, pharmacy systems, and inventory fields the false positive
262
+ * rate is unacceptably high.
263
+ *
264
+ * **When to use:** Dedicated DEA number entry fields — prescriber credentialing
265
+ * forms, controlled substance prescribing UI, pharmacy management systems where
266
+ * a DEA number is the specifically expected value. Do not enable on clinical
267
+ * notes, pharmacy dispensing free-text, or general healthcare free-text fields.
268
+ *
269
+ * Matches:
270
+ * `AB1234563` practitioner (C) + last name initial B
271
+ * `BX9876541` hospital (B) + last name initial X
272
+ */
273
+ DEA_NUMBER: "\\b[ABCDEFGHJKLMPRSTUX][A-Z]\\d{7}\\b",
274
+ /**
275
+ * SWIFT / BIC Code — Society for Worldwide Interbank Financial
276
+ * Telecommunication Business Identifier Code.
277
+ *
278
+ * **Why opt-in:**
279
+ * The pattern `[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?` matches any
280
+ * 8-letter word. Common English medical and technical terms —
281
+ * "nephropathy", "computing", "HYPERTENSION", "PENICILLIN" — all satisfy
282
+ * the 8-character uppercase letter constraint. In clinical notes or
283
+ * free-text fields this produces an extremely high false positive rate.
284
+ *
285
+ * **When to use:** Wire transfer instruction fields, SWIFT payment forms,
286
+ * or correspondent banking UI where a BIC code is the expected value.
287
+ * Do not enable on general-purpose or clinical free-text fields.
288
+ *
289
+ * Format: 8 or 11 alphanumeric characters.
290
+ * - Bank code: 4 uppercase letters e.g. `DEUT`
291
+ * - Country code: 2 uppercase letters e.g. `DE`
292
+ * - Location code: 2 alphanumeric chars e.g. `DB`
293
+ * - Branch code: 3 alphanumeric chars e.g. `BER` (optional)
294
+ *
295
+ * Matches:
296
+ * `DEUTDEDB` 8-character (head office)
297
+ * `DEUTDEDBBER` 11-character (branch)
298
+ * `BOFAUS3N` Bank of America US
299
+ * `CHASUS33` JPMorgan Chase US
300
+ */
301
+ SWIFT_BIC: "\\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?\\b",
302
+ /**
303
+ * National Provider Identifier (NPI) — 10-digit US healthcare provider ID.
304
+ *
305
+ * **Why opt-in:**
306
+ * The pattern `[12]\d{9}` matches any 10-digit number beginning with 1 or 2.
307
+ * In free-text clinical notes, dates with times, order numbers, reference IDs,
308
+ * and phone-number-adjacent strings frequently produce 10-digit sequences
309
+ * starting with 1 or 2. The false positive rate in unstructured clinical text
310
+ * is too high for this to be a default.
311
+ *
312
+ * **When to use:** Provider lookup fields, credentialing forms, NPI registry
313
+ * search inputs — fields where a 10-digit provider ID is the expected value.
314
+ * Do not enable on clinical note, encounter note, or general-purpose fields.
315
+ *
316
+ * NPIs are issued by CMS and are publicly searchable via the NPPES registry.
317
+ * An NPI in isolation is not sensitive — it is intentionally public. When an
318
+ * NPI appears alongside patient data it becomes a PHI linkage key.
319
+ *
320
+ * Two types:
321
+ * Type 1 (Individual): begins with 1 e.g. `1234567893`
322
+ * Type 2 (Organization): begins with 2 e.g. `2345678901`
323
+ */
324
+ NPI_NUMBER: "\\b[12]\\d{9}\\b",
325
+ /**
326
+ * Passport number — 1–2 uppercase letters followed by 6–9 digits.
327
+ *
328
+ * **Why opt-in:**
329
+ * The pattern `[A-Z]{1,2}[0-9]{6,9}` matches letter+digit combinations
330
+ * that are extremely common in clinical and product contexts:
331
+ * medication lot numbers (`AB123456`), lab specimen IDs (`A1234567`),
332
+ * ICD-10 codes with procedure modifiers, and equipment serial numbers
333
+ * all match this pattern. In clinical notes the false positive rate is high.
334
+ *
335
+ * **When to use:** Identity verification fields, KYC document entry forms,
336
+ * travel document upload flows — fields where a passport number is the
337
+ * expected value. Do not enable on clinical, product, or general free-text
338
+ * fields.
339
+ *
340
+ * Covers the most common formats:
341
+ * US: 1 letter + 8 digits e.g. `A12345678`
342
+ * EU: 2 letters + 7 digits e.g. `AB1234567`
343
+ * IN: 1 letter + 7 digits e.g. `A1234567`
344
+ */
345
+ PASSPORT_NUMBER: "\\b[A-Z]{1,2}[0-9]{6,9}\\b"
346
+ }), ce = 3e3, be = 1e5, de = (i) => Object.fromEntries(i.map((n) => [n.name, n.regex])), Se = (i = [], n = be, v, h) => {
347
+ const [A, g] = G(""), [$, O] = G([]), [N, S] = G(!1), s = M(null), I = JSON.stringify(i);
348
+ ee(() => {
349
+ let m = !1;
350
+ try {
351
+ s.current = new Worker(
352
+ new URL(
353
+ /* @vite-ignore */
354
+ "/assets/fieldshield.worker.js",
355
+ import.meta.url
356
+ ),
357
+ { type: "module" }
358
+ );
359
+ } catch (l) {
360
+ console.error(
361
+ "[FieldShield] Worker failed to initialize — falling back to a11yMode.",
362
+ l
363
+ ), queueMicrotask(() => S(!0));
364
+ return;
365
+ }
366
+ return s.current.onmessage = (l) => {
367
+ m || l.data?.type === "UPDATE" && typeof l.data.masked == "string" && Array.isArray(l.data.findings) && (g(l.data.masked), O(l.data.findings));
368
+ }, s.current.onerror = (l) => {
369
+ m || (console.error(
370
+ `[FieldShield] Worker runtime error: ${l.message ?? "unknown error"}`
371
+ ), g(""), O([]), h?.(l));
372
+ }, s.current.postMessage({
373
+ type: "CONFIG",
374
+ payload: {
375
+ defaultPatterns: te,
376
+ customPatterns: de([])
377
+ // Effect 2 delivers custom patterns after mount
378
+ }
379
+ }), () => {
380
+ m = !0, s.current?.terminate(), s.current = null;
381
+ };
382
+ }, []), ee(() => {
383
+ s.current && s.current.postMessage({
384
+ type: "CONFIG",
385
+ payload: {
386
+ defaultPatterns: te,
387
+ customPatterns: de(i)
388
+ }
389
+ });
390
+ }, [I]);
391
+ const R = z(
392
+ (m) => m.length > n ? (console.warn(
393
+ `[FieldShield] Input length ${m.length} exceeds maxProcessLength (${n}). Keystroke blocked to prevent unprotected data beyond the limit. Raise maxProcessLength if this field requires longer input.`
394
+ ), v?.(m.length, n), !1) : (s.current?.postMessage({
395
+ type: "PROCESS",
396
+ payload: { text: m }
397
+ }), !0),
398
+ [n, v]
399
+ ), T = z(() => new Promise((m, l) => {
400
+ if (!s.current) {
401
+ m("");
402
+ return;
403
+ }
404
+ const { port1: k, port2: B } = new MessageChannel(), L = setTimeout(() => {
405
+ k.close(), l(
406
+ new Error(
407
+ `[FieldShield] getSecureValue timed out after ${ce}ms.`
408
+ )
409
+ );
410
+ }, ce);
411
+ k.onmessage = (W) => {
412
+ clearTimeout(L), k.close(), m(W.data.text);
413
+ }, s.current.postMessage({ type: "GET_TRUTH" }, [B]);
414
+ }), []), U = z(() => {
415
+ s.current?.postMessage({ type: "PURGE" });
416
+ }, []);
417
+ return { masked: A, findings: $, processText: R, getSecureValue: T, purge: U, workerFailed: N };
418
+ }, ve = he(
419
+ ({
420
+ label: i,
421
+ type: n = "text",
422
+ placeholder: v,
423
+ customPatterns: h = [],
424
+ className: A,
425
+ style: g,
426
+ onChange: $,
427
+ a11yMode: O = !1,
428
+ onSensitiveCopyAttempt: N,
429
+ onSensitivePaste: S,
430
+ onFocus: s,
431
+ onBlur: I,
432
+ disabled: R = !1,
433
+ required: T = !1,
434
+ maxLength: U,
435
+ rows: m = 3,
436
+ inputMode: l = "text",
437
+ maxProcessLength: k = 1e5,
438
+ onMaxLengthExceeded: B,
439
+ onWorkerError: L
440
+ }, W) => {
441
+ const {
442
+ masked: V,
443
+ findings: Z,
444
+ processText: H,
445
+ getSecureValue: ae,
446
+ purge: se,
447
+ workerFailed: oe
448
+ } = Se(
449
+ h,
450
+ k,
451
+ B,
452
+ L
453
+ ), ue = O || oe, o = Z.length > 0, w = i ?? "Protected field", J = M(null), ne = M(null), K = M(null), u = M(""), C = ge(), _ = `${C}-warning`, j = `${C}-desc`;
454
+ me(
455
+ W,
456
+ () => ({
457
+ getSecureValue: ae,
458
+ purge: () => {
459
+ se(), H(""), u.current = "";
460
+ const e = J.current ?? ne.current;
461
+ e && (e.value = "", K.current && (K.current.textContent = `
462
+ `));
463
+ }
464
+ }),
465
+ [ae, se, H]
466
+ ), ee(() => {
467
+ $?.(V, Z);
468
+ }, [V, Z, $]);
469
+ const Y = (e, t, a) => {
470
+ if (!H(t)) {
471
+ const b = u.current.replace(/[^\n]/g, "x");
472
+ e.value = b;
473
+ const f = Math.min(a, u.current.length);
474
+ e.setSelectionRange(f, f);
475
+ return;
476
+ }
477
+ u.current = t;
478
+ const r = t.replace(/[^\n]/g, "x");
479
+ e.value = r, e.setSelectionRange(a, a), K.current && (K.current.textContent = r + `
480
+ `);
481
+ }, re = (e) => {
482
+ const t = e.target, a = t.value, p = t.selectionStart ?? a.length, r = u.current, b = a.length - r.length;
483
+ let f;
484
+ if (b > 0) {
485
+ const c = Math.max(0, p - b), E = a.slice(c, p);
486
+ f = r.slice(0, c) + E + r.slice(c);
487
+ } else if (b < 0) {
488
+ const c = p, E = c - b;
489
+ f = r.slice(0, c) + r.slice(E);
490
+ } else {
491
+ let c = -1, E = -1;
492
+ for (let y = 0; y < a.length; y++)
493
+ a[y] !== "x" && a[y] !== `
494
+ ` && (c === -1 && (c = y), E = y + 1);
495
+ if (c !== -1) {
496
+ const y = a.slice(c, E);
497
+ f = r.slice(0, c) + y + r.slice(E);
498
+ } else
499
+ f = r;
500
+ }
501
+ Y(t, f, p);
502
+ }, pe = (e) => {
503
+ const t = e.target.value;
504
+ u.current = t, H(t);
505
+ }, X = (e) => {
506
+ e.preventDefault();
507
+ const t = e.clipboardData.getData("text/plain"), a = e.target, p = a.selectionStart ?? 0, r = a.selectionEnd ?? 0, b = u.current, f = b.length - (r - p) + t.length;
508
+ if (f > k) {
509
+ console.warn(
510
+ `[FieldShield] Paste blocked — result length ${f} would exceed maxProcessLength (${k}). DOM unchanged.`
511
+ ), B?.(f, k);
512
+ return;
513
+ }
514
+ const c = b.slice(0, p) + t + b.slice(r), E = p + t.length;
515
+ if (Y(a, c, E), S && t) {
516
+ const y = [
517
+ ...Object.entries(te),
518
+ ...h.map((P) => [P.name, P.regex])
519
+ ], ie = [];
520
+ for (const [P, F] of y)
521
+ try {
522
+ ie.push([P, new RegExp(F, "gi")]);
523
+ } catch {
524
+ }
525
+ const q = [];
526
+ let Q = t;
527
+ for (const [P, F] of ie)
528
+ F.lastIndex = 0, F.test(t) && (q.push(P), F.lastIndex = 0, Q = Q.replace(
529
+ F,
530
+ (fe) => "█".repeat(fe.length)
531
+ ));
532
+ q.length > 0 && S({
533
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
534
+ fieldLabel: w,
535
+ findings: [...new Set(q)],
536
+ masked: Q,
537
+ eventType: "paste"
538
+ }) === !1 && Y(a, b, p);
539
+ }
540
+ }, x = (e) => {
541
+ e.preventDefault();
542
+ const t = e.target, a = t.selectionStart ?? 0, p = t.selectionEnd ?? u.current.length, r = V.slice(a, p), b = r.includes("█");
543
+ if (o && b ? (e.clipboardData.setData("text/plain", r), N?.({
544
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
545
+ fieldLabel: w,
546
+ findings: [...Z],
547
+ // snapshot — receiver must not hold a reference to internal state
548
+ masked: r,
549
+ eventType: e.type === "cut" ? "cut" : "copy"
550
+ })) : e.clipboardData.setData(
551
+ "text/plain",
552
+ u.current.slice(a, p)
553
+ ), e.type === "cut") {
554
+ const f = u.current.slice(0, a), c = u.current.slice(p);
555
+ u.current = f + c, H(u.current);
556
+ const E = "x".repeat(u.current.length);
557
+ t.value = E, t.setSelectionRange(a, a);
558
+ }
559
+ };
560
+ return ue ? /* @__PURE__ */ D(
561
+ "div",
562
+ {
563
+ className: `fieldshield-container${A ? ` ${A}` : ""}`,
564
+ style: g,
565
+ role: "group",
566
+ "aria-labelledby": C,
567
+ "data-disabled": R || void 0,
568
+ children: [
569
+ i && /* @__PURE__ */ d("label", { htmlFor: C, className: "fieldshield-label", children: i }),
570
+ /* @__PURE__ */ d("span", { id: j, className: "fieldshield-sr-only", children: "This field is protected. Sensitive data patterns will be detected and blocked from copying." }),
571
+ /* @__PURE__ */ d(
572
+ "input",
573
+ {
574
+ id: C,
575
+ ref: J,
576
+ type: "password",
577
+ className: "fieldshield-a11y-input",
578
+ placeholder: v,
579
+ onChange: pe,
580
+ onCopy: x,
581
+ onCut: x,
582
+ onPaste: X,
583
+ onFocus: s,
584
+ onBlur: I,
585
+ disabled: R,
586
+ required: T,
587
+ maxLength: U,
588
+ inputMode: l,
589
+ spellCheck: !1,
590
+ autoComplete: "off",
591
+ "aria-required": T,
592
+ "aria-label": i ? void 0 : w,
593
+ "aria-describedby": `${j} ${o ? _ : ""}`.trim(),
594
+ "aria-invalid": o ? "true" : "false",
595
+ "aria-errormessage": o ? _ : void 0
596
+ }
597
+ ),
598
+ /* @__PURE__ */ d(
599
+ "div",
600
+ {
601
+ id: _,
602
+ role: "status",
603
+ "aria-live": "polite",
604
+ "aria-atomic": "true",
605
+ className: "fieldshield-findings",
606
+ children: o && /* @__PURE__ */ D(le, { children: [
607
+ /* @__PURE__ */ d("span", { className: "fieldshield-warning-icon", "aria-hidden": "true", children: "⚠" }),
608
+ /* @__PURE__ */ D("span", { className: "fieldshield-warning-text", children: [
609
+ "Sensitive data detected. Clipboard blocked for:",
610
+ " "
611
+ ] }),
612
+ Z.map((e) => /* @__PURE__ */ d(
613
+ "span",
614
+ {
615
+ className: "fieldshield-tag",
616
+ "aria-label": `pattern: ${e}`,
617
+ children: e
618
+ },
619
+ e
620
+ ))
621
+ ] })
622
+ }
623
+ )
624
+ ]
625
+ }
626
+ ) : /* @__PURE__ */ D(
627
+ "div",
628
+ {
629
+ className: `fieldshield-container${A ? ` ${A}` : ""}`,
630
+ style: g,
631
+ role: "group",
632
+ "aria-labelledby": C,
633
+ "data-disabled": R || void 0,
634
+ children: [
635
+ i && /* @__PURE__ */ d("label", { htmlFor: C, className: "fieldshield-label", children: i }),
636
+ /* @__PURE__ */ d("span", { id: j, className: "fieldshield-sr-only", children: "Sensitive field. Input is protected. Sensitive data patterns will be detected and blocked from copying." }),
637
+ /* @__PURE__ */ D("div", { className: "fieldshield-field-wrapper", children: [
638
+ /* @__PURE__ */ d(
639
+ "div",
640
+ {
641
+ className: `fieldshield-mask-layer${o ? " fieldshield-mask-unsafe" : ""}`,
642
+ "aria-hidden": "true",
643
+ children: V || /* @__PURE__ */ d("span", { className: "fieldshield-placeholder", children: v })
644
+ }
645
+ ),
646
+ n === "textarea" && /* @__PURE__ */ d(
647
+ "div",
648
+ {
649
+ ref: K,
650
+ className: "fieldshield-grow",
651
+ "aria-hidden": "true"
652
+ }
653
+ ),
654
+ n === "textarea" ? /* @__PURE__ */ d(
655
+ "textarea",
656
+ {
657
+ ref: ne,
658
+ id: C,
659
+ className: "fieldshield-real-input",
660
+ placeholder: v,
661
+ onChange: re,
662
+ onPaste: X,
663
+ onCopy: x,
664
+ onCut: x,
665
+ onFocus: s,
666
+ onBlur: I,
667
+ disabled: R,
668
+ required: T,
669
+ maxLength: U,
670
+ rows: m,
671
+ inputMode: l,
672
+ spellCheck: !1,
673
+ autoComplete: "off",
674
+ "aria-required": T,
675
+ "aria-label": o ? `${w} — sensitive data detected` : `${w} — protected input`,
676
+ "aria-describedby": `${j} ${o ? _ : ""}`.trim(),
677
+ "aria-invalid": o ? "true" : "false",
678
+ "aria-errormessage": o ? _ : void 0
679
+ }
680
+ ) : /* @__PURE__ */ d(
681
+ "input",
682
+ {
683
+ ref: J,
684
+ id: C,
685
+ type: "text",
686
+ className: "fieldshield-real-input",
687
+ placeholder: v,
688
+ onChange: re,
689
+ onPaste: X,
690
+ onCopy: x,
691
+ onCut: x,
692
+ onFocus: s,
693
+ onBlur: I,
694
+ disabled: R,
695
+ required: T,
696
+ maxLength: U,
697
+ inputMode: l,
698
+ spellCheck: !1,
699
+ autoComplete: "off",
700
+ "aria-required": T,
701
+ "aria-label": o ? `${w} — sensitive data detected` : `${w} — protected input`,
702
+ "aria-describedby": `${j} ${o ? _ : ""}`.trim(),
703
+ "aria-invalid": o ? "true" : "false",
704
+ "aria-errormessage": o ? _ : void 0
705
+ }
706
+ )
707
+ ] }),
708
+ /* @__PURE__ */ d(
709
+ "div",
710
+ {
711
+ id: _,
712
+ role: "status",
713
+ "aria-live": "polite",
714
+ "aria-atomic": "true",
715
+ className: "fieldshield-findings",
716
+ children: o && /* @__PURE__ */ D(le, { children: [
717
+ /* @__PURE__ */ d("span", { className: "fieldshield-warning-icon", "aria-hidden": "true", children: "⚠" }),
718
+ /* @__PURE__ */ D("span", { className: "fieldshield-warning-text", children: [
719
+ "Sensitive data detected. Clipboard blocked for:",
720
+ " "
721
+ ] }),
722
+ Z.map((e) => /* @__PURE__ */ d(
723
+ "span",
724
+ {
725
+ className: "fieldshield-tag",
726
+ "aria-label": `pattern: ${e}`,
727
+ children: e
728
+ },
729
+ e
730
+ ))
731
+ ] })
732
+ }
733
+ )
734
+ ]
735
+ }
736
+ );
737
+ }
738
+ );
739
+ ve.displayName = "FieldShieldInput";
740
+ const Ce = (i = {}) => {
741
+ const { maxEvents: n = 20 } = i, [v, h] = G([]), A = M(0), g = z(
742
+ (N) => {
743
+ const S = ++A.current, s = (/* @__PURE__ */ new Date()).toLocaleTimeString();
744
+ h(
745
+ (I) => [{ ...N, id: S, timestamp: s }, ...I].slice(0, n)
746
+ );
747
+ },
748
+ [n]
749
+ ), $ = z(
750
+ (N) => (S) => {
751
+ const s = N === "paste" ? "PASTE_DETECTED" : S.eventType === "cut" ? "CUT_BLOCKED" : "COPY_BLOCKED";
752
+ g({
753
+ field: S.fieldLabel,
754
+ type: s,
755
+ findings: S.findings,
756
+ detail: `${S.masked.slice(0, 32)}${S.masked.length > 32 ? "…" : ""}`
757
+ });
758
+ },
759
+ [g]
760
+ ), O = z(() => {
761
+ h([]), A.current = 0;
762
+ }, []);
763
+ return { events: v, pushEvent: g, makeClipboardHandler: $, clearLog: O };
764
+ }, Te = async (i) => {
765
+ const n = Object.entries(i), v = await Promise.allSettled(
766
+ n.map(
767
+ ([, h]) => h.current?.getSecureValue() ?? Promise.resolve("")
768
+ )
769
+ );
770
+ return Object.fromEntries(
771
+ n.map(([h], A) => {
772
+ const g = v[A];
773
+ return g.status === "rejected" ? (console.warn(
774
+ `[FieldShield] collectSecureValues: field "${String(h)}" failed to retrieve value.`,
775
+ g.reason
776
+ ), [h, ""]) : [h, g.value];
777
+ })
778
+ );
779
+ }, ke = (i) => {
780
+ Object.values(i).forEach((n) => n.current?.purge());
781
+ };
782
+ export {
783
+ te as FIELDSHIELD_PATTERNS,
784
+ ve as FieldShieldInput,
785
+ ye as OPT_IN_PATTERNS,
786
+ Te as collectSecureValues,
787
+ ke as purgeSecureValues,
788
+ Se as useFieldShield,
789
+ Ce as useSecurityLog
790
+ };