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.
package/README.md ADDED
@@ -0,0 +1,1045 @@
1
+ # FieldShield
2
+
3
+ Sensitive input protection for React applications. Prevents DOM-based exposure of typed values, intercepts clipboard operations, and provides structured security logging for HIPAA and PCI-DSS compliance.
4
+
5
+ ```tsx
6
+ <FieldShieldInput
7
+ label="Social Security Number"
8
+ onSensitiveCopyAttempt={(e) => log(e)}
9
+ onSensitivePaste={(e) => false} // block sensitive pastes
10
+ />
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Contents
16
+
17
+ - [How it works](#how-it-works)
18
+ - [Installation](#installation)
19
+ - [Framework compatibility](#framework-compatibility)
20
+ - [Quick start](#quick-start)
21
+ - [Form library integration](#form-library-integration)
22
+ - [FieldShieldInput props](#fieldshieldinput-props)
23
+ - [Ref methods](#ref-methods)
24
+ - [Clipboard callbacks](#clipboard-callbacks)
25
+ - [maxProcessLength](#maxprocesslength)
26
+ - [Custom patterns](#custom-patterns)
27
+ - [Accessibility mode](#accessibility-mode)
28
+ - [useSecurityLog](#usesecuritylog)
29
+ - [collectSecureValues](#collectsecurevalues)
30
+ - [Built-in patterns](#built-in-patterns)
31
+ - [Content Security Policy](#content-security-policy)
32
+ - [Security architecture](#security-architecture)
33
+ - [Known limitations](#known-limitations)
34
+ - [Versioning and pattern updates](#versioning-and-pattern-updates)
35
+ - [TypeScript](#typescript)
36
+ - [Compliance notes](#compliance-notes)
37
+
38
+ ---
39
+
40
+ ## How it works
41
+
42
+ FieldShield protects against three attack vectors:
43
+
44
+ **DOM scraping** — Browser extensions, session recording tools (FullStory, LogRocket), and automated scrapers read `input.value` from the DOM. FieldShield stores the real value in an isolated Web Worker thread and writes only scrambled `x` characters to `input.value`. The DOM never contains the real value.
45
+
46
+ **Clipboard exfiltration** — Users accidentally copy sensitive text into LLMs, email clients, or unsecured applications. FieldShield intercepts copy and cut events and writes masked content (`█` characters) to the clipboard instead of the real value. The selection indices are preserved so partial copies also produce masked output.
47
+
48
+ **Paste exposure** — Sensitive data pasted from another source lands in the DOM and may be captured by recording tools. FieldShield intercepts paste events, scans the pasted content against all active patterns, and fires `onSensitivePaste` with the findings. Returning `false` from the callback blocks the paste entirely.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install fieldshield
56
+ ```
57
+
58
+ FieldShield requires React 18 or later.
59
+
60
+ Import the stylesheet once in your application entry point — FieldShield's component styles will not apply without it:
61
+
62
+ ```ts
63
+ import "fieldshield/dist/assets/fieldshield.css";
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Framework compatibility
69
+
70
+ FieldShield uses the `new URL('./fieldshield.worker.ts', import.meta.url)` pattern to instantiate its Web Worker. This is supported natively in Vite and in Webpack 5+ with no additional configuration.
71
+
72
+ ### Vite
73
+
74
+ Works out of the box. No configuration required.
75
+
76
+ ### Webpack 5
77
+
78
+ Works out of the box with Webpack 5's built-in Web Worker support.
79
+
80
+ ### Webpack 4
81
+
82
+ Requires `worker-loader`:
83
+
84
+ ```bash
85
+ npm install --save-dev worker-loader
86
+ ```
87
+
88
+ ```js
89
+ // webpack.config.js
90
+ module.exports = {
91
+ module: {
92
+ rules: [
93
+ {
94
+ test: /\.worker\.ts$/,
95
+ use: { loader: "worker-loader" },
96
+ },
97
+ ],
98
+ },
99
+ };
100
+ ```
101
+
102
+ ### Next.js
103
+
104
+ Next.js requires explicit worker configuration. Add the following to `next.config.js`:
105
+
106
+ ```js
107
+ // next.config.js
108
+ module.exports = {
109
+ webpack(config) {
110
+ config.output.publicPath = "/_next/";
111
+ return config;
112
+ },
113
+ };
114
+ ```
115
+
116
+ If you encounter issues with the worker URL resolution in Next.js, use the `NEXT_PUBLIC_` environment variable pattern to set the base URL explicitly, or open an issue — Next.js worker support is an active area of improvement.
117
+
118
+ ### Server-Side Rendering (SSR)
119
+
120
+ Web Workers are browser-only APIs. FieldShieldInput will throw if rendered on the server. Wrap it in a dynamic import with `ssr: false` in Next.js:
121
+
122
+ ```tsx
123
+ import dynamic from "next/dynamic";
124
+
125
+ const FieldShieldInput = dynamic(
126
+ () => import("fieldshield").then((m) => m.FieldShieldInput),
127
+ { ssr: false },
128
+ );
129
+ ```
130
+
131
+ ### Browser extension conflicts
132
+
133
+ Some browser extensions inject content into form fields and may conflict with FieldShieldInput's scrambling overlay:
134
+
135
+ - **Grammarly** — injects spell-check nodes that attempt to correct scrambled `x` characters. FieldShieldInput sets `spellcheck="false"` automatically, but if you see Grammarly interference add `data-gramm="false" data-gramm_editor="false"` to the container via the `className` or wrap with a div containing those attributes.
136
+ - **LastPass / 1Password** — these tools look for `type="password"` fields. FieldShieldInput is not a password field and will not trigger autofill, which is correct behavior — users should not autofill SSNs or clinical notes.
137
+
138
+ ### React 19
139
+
140
+ FieldShield works with React 19 without any configuration. The library uses
141
+ `forwardRef` internally which is deprecated but fully functional in React 19.
142
+ A migration to React 19's ref-as-prop pattern is planned for v1.1.
143
+
144
+ ---
145
+
146
+ ## Quick start
147
+
148
+ ```tsx
149
+ import { useRef } from "react";
150
+ import { FieldShieldInput } from "fieldshield";
151
+ import type { FieldShieldHandle } from "fieldshield";
152
+ import "fieldshield/dist/assets/fieldshield.css";
153
+
154
+ export function PatientForm() {
155
+ const ssnRef = useRef<FieldShieldHandle>(null);
156
+
157
+ const handleSubmit = async () => {
158
+ // Real value retrieved from isolated worker memory — never from the DOM
159
+ const ssn = await ssnRef.current?.getSecureValue();
160
+ await fetch("/api/patient", { body: JSON.stringify({ ssn }) });
161
+
162
+ // Zero out worker memory after submission
163
+ ssnRef.current?.purge();
164
+ };
165
+
166
+ return (
167
+ <FieldShieldInput
168
+ ref={ssnRef}
169
+ label="Social Security Number"
170
+ inputMode="numeric"
171
+ maxLength={11}
172
+ onSensitiveCopyAttempt={(e) => console.warn("Copy blocked:", e.findings)}
173
+ />
174
+ );
175
+ }
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Form library integration
181
+
182
+ ### React Hook Form
183
+
184
+ React Hook Form's `register()` expects to read `e.target.value` synchronously on every keystroke. Because FieldShieldInput writes only scrambled `x` characters to `input.value`, standard `register()` will validate `"xxxxxxxxxxxx"` rather than the real value.
185
+
186
+ The correct pattern is to use RHF's `Controller` component and validate on submit using `getSecureValue()`:
187
+
188
+ ```tsx
189
+ import { useRef } from "react";
190
+ import { useForm, Controller } from "react-hook-form";
191
+ import { FieldShieldInput, collectSecureValues } from "fieldshield";
192
+ import type { FieldShieldHandle } from "fieldshield";
193
+
194
+ export function PatientForm() {
195
+ const { handleSubmit, control, setError } = useForm();
196
+ const ssnRef = useRef<FieldShieldHandle>(null);
197
+
198
+ const onSubmit = async () => {
199
+ const { ssn } = await collectSecureValues({ ssn: ssnRef });
200
+
201
+ // Validate the real value here
202
+ if (!ssn.match(/^\d{3}-\d{2}-\d{4}$/)) {
203
+ setError("ssn", { message: "Invalid SSN format" });
204
+ return;
205
+ }
206
+
207
+ await fetch("/api/patient", { body: JSON.stringify({ ssn }) });
208
+ ssnRef.current?.purge();
209
+ };
210
+
211
+ return (
212
+ <form onSubmit={handleSubmit(onSubmit)}>
213
+ <Controller
214
+ name="ssn"
215
+ control={control}
216
+ render={() => (
217
+ <FieldShieldInput
218
+ ref={ssnRef}
219
+ label="Social Security Number"
220
+ inputMode="numeric"
221
+ maxLength={11}
222
+ />
223
+ )}
224
+ />
225
+ <button type="submit">Submit</button>
226
+ </form>
227
+ );
228
+ }
229
+ ```
230
+
231
+ The key shift is moving from synchronous per-keystroke validation to async on-submit validation. This is the correct mental model for any field where the value lives in isolated memory — validate at the point of use, not at the point of entry.
232
+
233
+ ### Why not validate on every keystroke?
234
+
235
+ Standard form libraries validate on `onChange` using `e.target.value`. Because the real value lives in worker memory and `e.target.value` only contains scrambled characters, per-keystroke validation of the real value would require a `GET_TRUTH` round-trip on every keystroke — one async operation per character typed. This creates unnecessary load on the worker and introduces the async overhead that RHF's synchronous model is designed to avoid.
236
+
237
+ The recommended pattern is: validate format constraints via `maxLength` and `inputMode` during input, then validate the real value's content on submit.
238
+
239
+ ### Formik
240
+
241
+ Same pattern as RHF — use `setFieldValue` in the submit handler after retrieving the real value:
242
+
243
+ ```tsx
244
+ const formik = useFormik({
245
+ initialValues: { ssn: "" },
246
+ onSubmit: async () => {
247
+ const { ssn } = await collectSecureValues({ ssn: ssnRef });
248
+ if (!isValidSSN(ssn)) {
249
+ formik.setFieldError("ssn", "Invalid SSN format");
250
+ return;
251
+ }
252
+ await submitToBackend({ ssn });
253
+ ssnRef.current?.purge();
254
+ },
255
+ });
256
+ ```
257
+
258
+ ### Zod
259
+
260
+ Zod validation works naturally at the submit boundary:
261
+
262
+ ```tsx
263
+ const schema = z.object({
264
+ ssn: z.string().regex(/^\d{3}-\d{2}-\d{4}$/, "Invalid SSN format"),
265
+ });
266
+
267
+ const onSubmit = async () => {
268
+ const values = await collectSecureValues({ ssn: ssnRef });
269
+ const result = schema.safeParse(values);
270
+ if (!result.success) {
271
+ // handle errors
272
+ return;
273
+ }
274
+ await submitToBackend(result.data);
275
+ };
276
+ ```
277
+
278
+ ---
279
+
280
+ ### `label`
281
+
282
+ `string` — optional
283
+
284
+ Visible label text rendered above the field and linked via `htmlFor`/`id`. Also used as the field identifier in clipboard event payloads. When omitted no `<label>` is rendered and the field falls back to `"Protected field"` for screen reader announcements.
285
+
286
+ ### `type`
287
+
288
+ `"text" | "textarea"` — default `"text"`
289
+
290
+ Renders a single-line `<input>` or a multi-line `<textarea>`. Textarea mode enables auto-grow — the field expands vertically as the user types past the initial height.
291
+
292
+ ### `placeholder`
293
+
294
+ `string` — optional
295
+
296
+ Forwarded to the native `placeholder` attribute. Displayed in the mask layer when the field is empty.
297
+
298
+ ### `disabled`
299
+
300
+ `boolean` — default `false`
301
+
302
+ Disables the field. Sets `data-disabled` on the container for CSS styling hooks.
303
+
304
+ ### `required`
305
+
306
+ `boolean` — default `false`
307
+
308
+ Sets `aria-required` on the input so screen readers announce the field as mandatory.
309
+
310
+ ### `maxLength`
311
+
312
+ `number` — optional
313
+
314
+ Native HTML `maxLength`. Use this for structured fields with known lengths — SSN (11), credit card (19), IBAN (34 max). Enforced by the browser before FieldShield processes the input.
315
+
316
+ ### `rows`
317
+
318
+ `number` — default `3`
319
+
320
+ Initial visible row count. Only applies when `type="textarea"`. The field still auto-grows beyond this value.
321
+
322
+ ### `inputMode`
323
+
324
+ `"text" | "numeric" | "decimal" | "tel" | "email" | "search" | "url" | "none"` — default `"text"`
325
+
326
+ Mobile keyboard hint. Does not affect value handling — the field always operates as `type="text"` internally to preserve DOM scrambling.
327
+
328
+ Use this instead of `type="number"` or `type="email"` — those change browser validation and value parsing in ways that break DOM scrambling.
329
+
330
+ ```tsx
331
+ <FieldShieldInput inputMode="numeric" label="SSN" />
332
+ <FieldShieldInput inputMode="tel" label="Phone" />
333
+ ```
334
+
335
+ ### `className`
336
+
337
+ `string` — optional
338
+
339
+ Additional CSS class applied to the outermost container `<div>`, merged with the internal `fieldshield-container` class.
340
+
341
+ ### `style`
342
+
343
+ `React.CSSProperties` — optional
344
+
345
+ Inline styles applied to the outermost container `<div>`.
346
+
347
+ ### `onChange`
348
+
349
+ `(masked: string, findings: string[]) => void` — optional
350
+
351
+ Fires after each worker UPDATE response — whenever the masked value or findings change. Receives the masked display string and the current findings array. Never receives the real value.
352
+
353
+ ```tsx
354
+ <FieldShieldInput
355
+ label="Notes"
356
+ onChange={(masked, findings) => {
357
+ if (findings.length > 0) setHasSensitiveData(true);
358
+ }}
359
+ />
360
+ ```
361
+
362
+ ### `a11yMode`
363
+
364
+ `boolean` — default `false`
365
+
366
+ Disables DOM scrambling and renders a native `type="password"` input instead. Pattern detection and clipboard protection remain fully active.
367
+
368
+ Use this for WCAG 2.1 AA / Section 508 compliance — screen readers handle `type="password"` natively and cannot interact with the scrambled overlay used in standard mode. See [Accessibility mode](#accessibility-mode).
369
+
370
+ ### `customPatterns`
371
+
372
+ `CustomPattern[]` — optional
373
+
374
+ Additional sensitive-data patterns layered on top of the built-in defaults. See [Custom patterns](#custom-patterns).
375
+
376
+ ### `maxProcessLength`
377
+
378
+ `number` — default `100000`
379
+
380
+ Maximum number of characters sent to the worker for pattern detection. If the user types or pastes beyond this limit the input is **blocked** — the field reverts to its previous value.
381
+
382
+ Blocking rather than truncating is intentional. Truncation would create a blind spot where sensitive data beyond the limit is never scanned or protected.
383
+
384
+ > **Important:** Always wire up `onMaxLengthExceeded` for any field that uses `maxProcessLength`. Without it, the field silently stops accepting input with no explanation to the user.
385
+
386
+ ```tsx
387
+ <FieldShieldInput
388
+ label="Clinical Notes"
389
+ type="textarea"
390
+ maxProcessLength={50_000}
391
+ onMaxLengthExceeded={(length, limit) =>
392
+ setError(`Maximum ${limit.toLocaleString()} characters reached`)
393
+ }
394
+ />
395
+ ```
396
+
397
+ This is distinct from `maxLength` — `maxLength` restricts the browser input, `maxProcessLength` caps worker processing. For structured fields with known lengths, use `maxLength`. For free-text fields where longer input is valid but should be bounded, use `maxProcessLength`.
398
+
399
+ ### `onMaxLengthExceeded`
400
+
401
+ `(length: number, limit: number) => void` — optional
402
+
403
+ Called when input is blocked because it exceeds `maxProcessLength`. Use this to surface a character count warning or error message to the user.
404
+
405
+ A `console.warn` fires automatically even without this callback so developers see the block in DevTools.
406
+
407
+ ### `onWorkerError`
408
+
409
+ `(error: ErrorEvent) => void` — optional
410
+
411
+ Called when the Web Worker encounters a runtime error. When this fires, FieldShieldInput has already reset `masked` and `findings` to empty so the field does not freeze showing stale warnings.
412
+
413
+ The worker is not terminated on error — a transient error may not affect subsequent messages. If errors persist, surface a warning and ask the user to refresh.
414
+
415
+ > **Note:** If the worker fails to initialize entirely (e.g. due to a strict CSP), the component automatically falls back to `a11yMode` — this callback is not called in that case. The fallback is silent by design but logged to `console.error`.
416
+
417
+ ```tsx
418
+ <FieldShieldInput
419
+ label="SSN"
420
+ onWorkerError={(e) => {
421
+ console.error("Worker error:", e.message);
422
+ setFieldError("Worker unavailable — please refresh");
423
+ }}
424
+ />
425
+ ```
426
+
427
+ ### `onFocus`
428
+
429
+ `(e: React.FocusEvent) => void` — optional
430
+
431
+ Forwarded from the underlying input element.
432
+
433
+ ### `onBlur`
434
+
435
+ `(e: React.FocusEvent) => void` — optional
436
+
437
+ Forwarded from the underlying input element.
438
+
439
+ ### `onSensitiveCopyAttempt`
440
+
441
+ `(event: SensitiveClipboardEvent) => void` — optional
442
+
443
+ Fired when the user copies or cuts from the field while sensitive patterns are present. The clipboard receives the masked text instead of the real value. Use this to surface a toast notification or write a security audit log.
444
+
445
+ ### `onSensitivePaste`
446
+
447
+ `(event: SensitiveClipboardEvent) => boolean | void` — optional
448
+
449
+ Fired when the user pastes content that contains sensitive patterns.
450
+
451
+ Return `false` to block the paste — the field reverts to its previous value and the clipboard content is discarded. Return nothing or `true` to allow the paste to proceed.
452
+
453
+ ```tsx
454
+ // Block sensitive pastes
455
+ onSensitivePaste={(e) => {
456
+ auditLog(e);
457
+ return false;
458
+ }}
459
+
460
+ // Allow sensitive pastes but log them
461
+ onSensitivePaste={(e) => {
462
+ auditLog(e);
463
+ // return nothing — paste proceeds
464
+ }}
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Ref methods
470
+
471
+ Attach a ref typed as `FieldShieldHandle` to access imperative methods.
472
+
473
+ ```tsx
474
+ const ref = useRef<FieldShieldHandle>(null);
475
+ <FieldShieldInput ref={ref} label="SSN" />;
476
+ ```
477
+
478
+ ### `getSecureValue()`
479
+
480
+ `() => Promise<string>`
481
+
482
+ Retrieves the real, unmasked value from the worker's isolated memory via a private `MessageChannel`. The value travels point-to-point — browser extensions monitoring `postMessage` on the page cannot intercept it.
483
+
484
+ Rejects with a timeout error if the worker does not respond within 3 seconds.
485
+
486
+ **Always handle the rejection.** A rejected `getSecureValue()` means the worker is unavailable — the field value is lost and the form cannot be submitted safely. Do not silently swallow the error.
487
+
488
+ ```ts
489
+ const handleSubmit = async () => {
490
+ try {
491
+ const value = await ref.current?.getSecureValue();
492
+ await fetch("/api/save", { body: JSON.stringify({ value }) });
493
+ } catch (err) {
494
+ // Worker timed out or was terminated — surface an error to the user
495
+ setSubmitError(
496
+ "Unable to retrieve field value securely. Please refresh and try again.",
497
+ );
498
+ return;
499
+ }
500
+ };
501
+ ```
502
+
503
+ **Session timeout pattern** — for HIPAA compliance, call `purgeSecureValues` when the session expires to ensure worker memory is zeroed before the user is logged out:
504
+
505
+ ```ts
506
+ // On session timeout or logout
507
+ const handleSessionEnd = () => {
508
+ purgeSecureValues(refs); // zero all workers simultaneously
509
+ redirectToLogin();
510
+ };
511
+ ```
512
+
513
+ ### `purge()`
514
+
515
+ `() => void`
516
+
517
+ Zeros out the stored value in worker memory. Call this immediately after `getSecureValue()` resolves and the data has been sent to your backend.
518
+
519
+ ```ts
520
+ const value = await ref.current?.getSecureValue();
521
+ await sendToBackend(value);
522
+ ref.current?.purge(); // fire and forget
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Clipboard callbacks
528
+
529
+ Both `onSensitiveCopyAttempt` and `onSensitivePaste` receive a `SensitiveClipboardEvent` payload:
530
+
531
+ ```ts
532
+ interface SensitiveClipboardEvent {
533
+ timestamp: string; // ISO 8601
534
+ fieldLabel: string; // the label prop value
535
+ findings: string[]; // e.g. ["SSN", "EMAIL"]
536
+ masked: string; // masked preview with █ characters
537
+ eventType: "copy" | "cut" | "paste";
538
+ }
539
+ ```
540
+
541
+ `masked` contains only the selected/pasted portion with sensitive spans replaced by `█`. The length is preserved so the structure is visible — `"SSN: ███-██-████"` rather than a uniform block.
542
+
543
+ ---
544
+
545
+ ## maxProcessLength
546
+
547
+ The default of `100_000` characters is large enough for legitimate clinical notes and free-text fields while protecting against denial-of-service via adversarially crafted regex inputs.
548
+
549
+ For structured fields with known maximum lengths, use `maxLength` instead — the browser enforces it before FieldShield processes anything, which is more efficient.
550
+
551
+ ```tsx
552
+ // Structured field — browser enforces 11 chars, worker never sees more
553
+ <FieldShieldInput label="SSN" maxLength={11} />
554
+
555
+ // Free text — worker processes up to 100k chars
556
+ <FieldShieldInput label="Clinical Notes" type="textarea" />
557
+
558
+ // Custom limit
559
+ <FieldShieldInput
560
+ label="Notes"
561
+ maxProcessLength={50_000}
562
+ onMaxLengthExceeded={(length, limit) =>
563
+ setError(`Input too long — maximum ${limit.toLocaleString()} characters`)
564
+ }
565
+ />
566
+ ```
567
+
568
+ ---
569
+
570
+ ## Custom patterns
571
+
572
+ Pass an array of `CustomPattern` objects to detect additional sensitive data types specific to your application.
573
+
574
+ ```tsx
575
+ <FieldShieldInput
576
+ label="Employee Record"
577
+ customPatterns={[
578
+ { name: "EMPLOYEE_ID", regex: "EMP-\\d{6}" },
579
+ { name: "BADGE_NUMBER", regex: "\\bBDG-[A-Z]{2}\\d{4}\\b" },
580
+ ]}
581
+ onSensitiveCopyAttempt={(e) => log(e.findings)}
582
+ />
583
+ ```
584
+
585
+ Custom patterns are layered on top of the built-in defaults — both sets run on every keystroke. If a custom pattern has the same name as a built-in, it overrides the built-in for that field.
586
+
587
+ ```ts
588
+ interface CustomPattern {
589
+ name: string; // shown in findings arrays
590
+ regex: string; // regex source string — no delimiters, no flags
591
+ // use double backslashes: "\\d{6}" not "\d{6}"
592
+ }
593
+ ```
594
+
595
+ The worker applies `gi` flags automatically. Order is preserved — patterns run in array order.
596
+
597
+ ---
598
+
599
+ ## Accessibility mode
600
+
601
+ Standard mode uses a DOM scrambling overlay that is invisible to sighted users but incompatible with some screen readers. Enable `a11yMode` for WCAG 2.1 AA / Section 508 compliance:
602
+
603
+ ```tsx
604
+ <FieldShieldInput ref={ref} label="SSN" a11yMode />
605
+ ```
606
+
607
+ In `a11yMode`:
608
+
609
+ - A native `type="password"` input is rendered
610
+ - The browser's built-in password masking handles visual output
611
+ - Pattern detection still runs through the worker on every keystroke
612
+ - Clipboard protection remains fully active
613
+ - The scrambling overlay is not rendered
614
+
615
+ Use `a11yMode` when your users rely on screen readers (VoiceOver, NVDA, JAWS) or when WCAG compliance is required.
616
+
617
+ ---
618
+
619
+ ## useSecurityLog
620
+
621
+ Maintains a capped, auto-timestamped log of FieldShield security events suitable for real-time audit displays and HIPAA audit trail requirements.
622
+
623
+ ```tsx
624
+ import { useSecurityLog } from "fieldshield";
625
+
626
+ const { events, makeClipboardHandler, pushEvent, clearLog } = useSecurityLog({
627
+ maxEvents: 20, // default
628
+ });
629
+
630
+ <FieldShieldInput
631
+ label="SSN"
632
+ onSensitiveCopyAttempt={makeClipboardHandler("copy_cut")}
633
+ onSensitivePaste={makeClipboardHandler("paste")}
634
+ />;
635
+
636
+ // Display the log
637
+ {
638
+ events.map((ev) => (
639
+ <div key={ev.id}>
640
+ {ev.timestamp} — {ev.type} — {ev.field} — {ev.findings.join(", ")}
641
+ </div>
642
+ ));
643
+ }
644
+ ```
645
+
646
+ ### `makeClipboardHandler(context)`
647
+
648
+ Returns a ready-to-wire `SensitiveClipboardEvent` handler.
649
+
650
+ - Pass `"copy_cut"` for `onSensitiveCopyAttempt` — inspects `e.eventType` internally to distinguish `COPY_BLOCKED` from `CUT_BLOCKED`
651
+ - Pass `"paste"` for `onSensitivePaste` — maps to `PASTE_DETECTED`
652
+
653
+ ### `pushEvent(event)`
654
+
655
+ Push any event manually — use for `SUBMIT` and `PURGE` events:
656
+
657
+ ```ts
658
+ pushEvent({
659
+ field: "All fields",
660
+ type: "SUBMIT",
661
+ findings: [],
662
+ detail: "3 fields submitted",
663
+ });
664
+ ```
665
+
666
+ ### `clearLog()`
667
+
668
+ Empties the events array and resets the ID counter.
669
+
670
+ ### Event types
671
+
672
+ `COPY_BLOCKED` | `CUT_BLOCKED` | `PASTE_DETECTED` | `SUBMIT` | `PURGE` | `CUSTOM`
673
+
674
+ ### Event shape
675
+
676
+ ```ts
677
+ interface SecurityEvent {
678
+ id: number; // auto-incrementing, stable React key
679
+ timestamp: string; // from Date.toLocaleTimeString()
680
+ field: string; // field label or custom identifier
681
+ type: SecurityEventType;
682
+ findings: string[]; // pattern names active at time of event
683
+ detail?: string; // truncated masked preview (32 chars)
684
+ }
685
+ ```
686
+
687
+ ---
688
+
689
+ ## collectSecureValues
690
+
691
+ Retrieves real values from multiple FieldShieldInput fields in parallel via `Promise.allSettled`. No plaintext exists on the main thread until this call resolves.
692
+
693
+ ```tsx
694
+ import { useRef } from "react";
695
+ import {
696
+ FieldShieldInput,
697
+ collectSecureValues,
698
+ purgeSecureValues,
699
+ } from "fieldshield";
700
+ import type { FieldShieldHandle } from "fieldshield";
701
+
702
+ export function PatientForm() {
703
+ const ssnRef = useRef<FieldShieldHandle>(null);
704
+ const notesRef = useRef<FieldShieldHandle>(null);
705
+ const emailRef = useRef<FieldShieldHandle>(null);
706
+
707
+ const refs = { ssn: ssnRef, notes: notesRef, email: emailRef };
708
+
709
+ const handleSubmit = async () => {
710
+ const values = await collectSecureValues(refs);
711
+ // values = { ssn: "123-45-6789", notes: "...", email: "..." }
712
+
713
+ await fetch("/api/patient", {
714
+ method: "POST",
715
+ body: JSON.stringify(values),
716
+ });
717
+
718
+ purgeSecureValues(refs); // zero all workers simultaneously
719
+ };
720
+
721
+ return (
722
+ <>
723
+ <FieldShieldInput ref={ssnRef} label="SSN" />
724
+ <FieldShieldInput ref={notesRef} label="Clinical Notes" type="textarea" />
725
+ <FieldShieldInput ref={emailRef} label="Email" />
726
+ <button onClick={handleSubmit}>Submit</button>
727
+ </>
728
+ );
729
+ }
730
+ ```
731
+
732
+ Null or unmounted refs resolve to `""` rather than throwing — a missing optional field never blocks form submission. Rejected fields also resolve to `""` with a `console.warn` identifying the field name.
733
+
734
+ `purgeSecureValues` calls `purge()` on every ref simultaneously. It is fire-and-forget — no await needed. The PURGE message is processed after the GET_TRUTH reply because both travel through the same worker message queue in order.
735
+
736
+ ---
737
+
738
+ ## Built-in patterns
739
+
740
+ All patterns apply `gi` flags — case-insensitive and global. Patterns are designed for a security context: false negative rate is minimized over false positive rate because a missed sensitive value is worse than a false positive that briefly highlights a non-sensitive number.
741
+
742
+ **13 built-in patterns active by default.** Five additional patterns (`IBAN`, `DEA_NUMBER`, `SWIFT_BIC`, `NPI_NUMBER`, `PASSPORT_NUMBER`) are available as [opt-in patterns](#opt-in-patterns) due to high false positive rates in free-text fields.
743
+
744
+ ### PII patterns
745
+
746
+ | Pattern | Matches |
747
+ | --------------- | ------------------------------------------------------------------------------------ |
748
+ | `SSN` | `123-45-6789` · `123 45 6789` · `123.45.6789` · `123456789` |
749
+ | `EMAIL` | RFC 5321 compatible — `user@example.com`, plus addressing, subdomains |
750
+ | `PHONE` | US all formats · `+44` UK · `+91` India · `+353` Ireland · `+86` China and more |
751
+ | `CREDIT_CARD` | Visa 16-digit · Mastercard · Amex 15-digit — with or without spaces/hyphens |
752
+ | `DATE_OF_BIRTH` | `MM/DD/YYYY` · `MM-DD-YYYY` · `MM.DD.YYYY` · `YYYY-MM-DD` · `YYYY/MM/DD` (1900–2099) |
753
+ | `TAX_ID` | EIN `12-3456789` · 9-digit no separator |
754
+
755
+ ### Healthcare and international identifiers
756
+
757
+ | Pattern | Matches |
758
+ | -------- | --------------------------------------------------------------------------------- |
759
+ | `UK_NIN` | UK National Insurance Number — `AB 12 34 56 C` (spaced) or `AB123456C` (compact) |
760
+
761
+ ### Credential patterns
762
+
763
+ These patterns are designed for developer-facing inputs — config panels, support chat, API key management UIs. Consumer-facing deployments can safely ignore them — a user entering their SSN will never trigger `GITHUB_TOKEN` or `JWT`.
764
+
765
+ | Pattern | Matches |
766
+ | ------------------- | --------------------------------------------------------------------------- |
767
+ | `AI_API_KEY` | OpenAI `sk-` (all generations) · Anthropic `sk-ant-api03-` · Google `AIza` |
768
+ | `AWS_ACCESS_KEY` | `AKIA` permanent · `ASIA` temporary credential prefix |
769
+ | `GITHUB_TOKEN` | `ghp_` · `gho_` · `ghs_` · `ghu_` · `github_pat_` |
770
+ | `STRIPE_KEY` | `sk_live_` · `sk_test_` · `pk_live_` · `pk_test_` · `rk_live_` · `rk_test_` |
771
+ | `JWT` | Three base64url segments starting with `eyJ` |
772
+ | `PRIVATE_KEY_BLOCK` | `-----BEGIN [RSA\|EC\|OPENSSH] PRIVATE KEY-----` |
773
+
774
+ ### Overriding a built-in pattern
775
+
776
+ Pass a custom pattern with the same name to override the built-in for that field:
777
+
778
+ ```tsx
779
+ // Replace the built-in SSN pattern with a stricter version for this field
780
+ <FieldShieldInput
781
+ customPatterns={[{ name: "SSN", regex: "\\b\\d{3}-\\d{2}-\\d{4}\\b" }]}
782
+ />
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Opt-in patterns
788
+
789
+ Some patterns are too broad to enable on every field. These four are excluded from the defaults because their regex structure matches common non-sensitive strings in clinical notes, pharmacy systems, and general free-text:
790
+
791
+ | Pattern | Why it's opt-in |
792
+ | ----------------- | ----------------------------------------------------------------------------------------- |
793
+ | `IBAN` | Two letters + two digits + alphanumeric groups — matches lab accession numbers, lot codes |
794
+ | `DEA_NUMBER` | Two letters + seven digits — matches any pharmaceutical lot number (`AB1234567`) |
795
+ | `SWIFT_BIC` | Eight uppercase letters — matches common words ("NEPHROPATHY", "PENICILLIN") |
796
+ | `NPI_NUMBER` | Ten digits starting with 1 or 2 — matches timestamps, order IDs, phone numbers |
797
+ | `PASSPORT_NUMBER` | One or two letters + six to nine digits — matches ICD-10 codes, specimen IDs |
798
+
799
+ > **Only add these to fields where that specific data type is the expected input.** Adding `NPI_NUMBER` to a clinical notes field will flag nearly every number entered.
800
+
801
+ ### Usage
802
+
803
+ `OPT_IN_PATTERNS` values are regex source strings (the same type as `customPatterns.regex`), not `RegExp` objects. Pass them directly:
804
+
805
+ ```tsx
806
+ import { FieldShieldInput, OPT_IN_PATTERNS } from "fieldshield";
807
+
808
+ // Wire transfer form — a BIC code is the only expected value here
809
+ <FieldShieldInput
810
+ label="Bank (SWIFT/BIC)"
811
+ customPatterns={[{ name: "SWIFT_BIC", regex: OPT_IN_PATTERNS.SWIFT_BIC }]}
812
+ />
813
+
814
+ // Provider credentialing form — an NPI is the only expected value here
815
+ <FieldShieldInput
816
+ label="Provider NPI"
817
+ customPatterns={[{ name: "NPI_NUMBER", regex: OPT_IN_PATTERNS.NPI_NUMBER }]}
818
+ />
819
+ ```
820
+
821
+ The 14 built-in patterns stay active — `customPatterns` layers on top of them, it does not replace them.
822
+
823
+ ---
824
+
825
+ ## Content Security Policy
826
+
827
+ FieldShield's worker isolation guarantee can be enforced at the infrastructure level using Content Security Policy headers. Add the following directives to your CSP:
828
+
829
+ ```
830
+ Content-Security-Policy:
831
+ worker-src 'self';
832
+ script-src 'self';
833
+ ```
834
+
835
+ **`worker-src 'self' blob:`** — restricts Web Workers to same-origin scripts and blob URLs. The `blob:` source is required if you use the pre-compiled worker option (v1.1 roadmap). If you are certain you will only ever use the default source-file worker, `worker-src 'self'` without `blob:` is stricter.
836
+
837
+ **`script-src 'self'`** — restricts all script execution to same-origin. Combined with `worker-src`, this ensures neither the main thread nor the worker can load or execute scripts from external origins.
838
+
839
+ ### No-network guarantee
840
+
841
+ The FieldShield worker makes no network requests of any kind. It contains no calls to `fetch()`, `XMLHttpRequest`, `WebSocket`, `EventSource`, or `navigator.sendBeacon()`. Communication is exclusively via `postMessage` with the main thread.
842
+
843
+ This guarantee is verifiable by inspecting `fieldshield.worker.ts` directly — the file has zero imports and zero network API calls. The `@security NO NETWORK ACCESS` comment at the top of the worker file is intended for auditors who need documented evidence of this property.
844
+
845
+ ### Full recommended CSP for FieldShield deployments
846
+
847
+ ```
848
+ Content-Security-Policy:
849
+ default-src 'self';
850
+ script-src 'self';
851
+ worker-src 'self' blob:;
852
+ connect-src 'self' https://your-api.example.com;
853
+ style-src 'self' 'unsafe-inline';
854
+ img-src 'self' data:;
855
+ frame-ancestors 'none';
856
+ ```
857
+
858
+ Adjust `connect-src` to include only the API endpoints your application needs. The `frame-ancestors 'none'` directive prevents clickjacking attacks on forms containing sensitive fields.
859
+
860
+ ---
861
+
862
+ ## Security architecture
863
+
864
+ ### Web Worker isolation
865
+
866
+ The real input value (`internalTruth`) lives exclusively in a dedicated Web Worker thread. It is never serialized to the main thread except through a private `MessageChannel` in response to an explicit `GET_TRUTH` message. Browser extensions monitoring `postMessage` on the page cannot intercept `MessageChannel` port messages because they are point-to-point, not broadcast.
867
+
868
+ ### DOM scrambling
869
+
870
+ Every non-newline character in `input.value` is replaced with `x`. The DOM always contains scrambled content. Screen scrapers, browser extensions reading `.value`, and session recording tools see only `xxxxxxxxxx`. The `x` characters are replaced by an absolutely-positioned transparent layer — sighted users see the masked output from the worker, keyboard events and cursor positioning happen on the transparent real input.
871
+
872
+ ### Clipboard masking
873
+
874
+ Copy and cut events are intercepted via `onCopy`/`onCut` handlers. When sensitive patterns are present, `e.clipboardData.setData()` writes the masked string (with `█` characters) rather than the real value. The selection range is mapped from the scrambled DOM coordinates to the real value coordinates to produce accurate partial-selection masking.
875
+
876
+ ### Paste scanning
877
+
878
+ Paste events are intercepted before the browser inserts clipboard content. The pasted text is scanned synchronously against all active patterns using the same pattern source strings the worker uses — guaranteeing the pre-scan is always in sync with the worker scan. The `onSensitivePaste` callback fires before the paste lands, allowing real-time audit logging.
879
+
880
+ ### Memory purge
881
+
882
+ The `PURGE` message zeros `internalTruth` in worker memory and posts a `PURGED` confirmation. This provides demonstrable evidence of data disposal for HIPAA and PCI-DSS compliance audits.
883
+
884
+ ---
885
+
886
+ ## Known limitations
887
+
888
+ ### `realValueRef` on the main thread
889
+
890
+ While the user is actively typing, the real value exists in both worker memory (`internalTruth`) and a React ref on the main thread (`realValueRef`). This ref is required to reconstruct the real value from DOM events — without it, character-by-character editing would be impossible.
891
+
892
+ In React DevTools, a sufficiently privileged browser extension, or a debugger attached to the page, `realValueRef.current` is readable. The worker isolation guarantee applies fully at rest — when the user is not typing — and provides strong protection against passive DOM scraping. It does not protect against an active attacker with debugger access to the page.
893
+
894
+ ### `getSecureValue()` on unmount
895
+
896
+ If `getSecureValue()` is called after the component unmounts, the worker has already been terminated and `workerRef.current` is null. The call resolves to `""` immediately. Always call `getSecureValue()` before triggering any navigation or unmount.
897
+
898
+ ### `onBlur` / `handleBlur` zeroing `realValueRef`
899
+
900
+ FieldShield does not zero `realValueRef` on blur. The async `getSecureValue()` call on re-focus creates a race condition — the user might focus the field and immediately submit before the worker responds. The value is preserved across focus changes. Document this expectation in your threat model if required by your compliance framework.
901
+
902
+ ### IME composition (CJK input)
903
+
904
+ Composed input via Input Method Editors (Chinese, Japanese, Korean) is not supported in v1. Characters entered via IME may not be reconstructed correctly in `realValueRef`. Planned for v1.1.
905
+
906
+ ### Voice dictation
907
+
908
+ Third-party voice dictation software (Dragon NaturallySpeaking, etc.) injects text via OS-level events rather than standard DOM events. FieldShield cannot guarantee correct value reconstruction for voice-dictated input.
909
+
910
+ ### Drag-and-drop text
911
+
912
+ Dragging text within the field to rearrange it is not supported. The reconstructed real value may be incorrect after an in-field drag operation.
913
+
914
+ ### Tab character
915
+
916
+ Tab characters in textarea fields produce a visual drift between the mask layer and the real input cursor position. The stored real value remains correct — only the visual alignment is affected.
917
+
918
+ ### No `name` prop — native form submission not supported
919
+
920
+ FieldShieldInput does not accept a `name` prop and does not support native HTML form submission via `<form>`. Because `input.value` always contains scrambled `x` characters, a native form submit would send garbage to the server.
921
+
922
+ Always use `getSecureValue()` or `collectSecureValues()` on submit — never rely on the DOM value.
923
+
924
+ ### No `id` prop override
925
+
926
+ FieldShieldInput generates its own stable `id` via React's `useId()` hook to prevent collisions when multiple instances share a page. You cannot set a custom `id` on the underlying input element. If you need to target the input externally (e.g. for testing selectors), use `aria-label` or the container's `className` prop instead.
927
+
928
+ ### `onCopy` and `onCut` props not forwarded
929
+
930
+ FieldShieldInput intercepts `copy` and `cut` events internally to implement clipboard masking. Consumer-provided `onCopy` and `onCut` props are not forwarded — they would silently do nothing. Use `onSensitiveCopyAttempt` instead, which fires after the masking has been applied and the clipboard has been written.
931
+
932
+ ### Cross-field sensitive data combination
933
+
934
+ FieldShield detects sensitive patterns within a single field. It does not detect combinations across fields — for example, a first name in one field and an SSN in another that together constitute a HIPAA minimum necessary data set. Cross-field combination detection requires application-level logic. Use `onChange` to receive findings from each field and implement your own combination rules.
935
+
936
+ ```tsx
937
+ // Example: detect SSN in any field on the form
938
+ const [formHasSensitiveData, setFormHasSensitiveData] = useState(false);
939
+
940
+ <FieldShieldInput
941
+ label="Notes"
942
+ onChange={(_, findings) => {
943
+ if (findings.includes("SSN")) setFormHasSensitiveData(true);
944
+ }}
945
+ />;
946
+ ```
947
+
948
+ ### Names, addresses, and unstructured PHI
949
+
950
+ FieldShield detects structured sensitive data — values with a recognisable format like SSNs, credit card numbers, and API keys. It does not detect unstructured PHI such as:
951
+
952
+ - Patient names (`"John Smith"`)
953
+ - Street addresses (`"123 Main Street, Boston MA"`)
954
+ - Facility names, physician names, employer names
955
+ - Free-text clinical descriptions
956
+
957
+ These cannot be detected reliably with regex — they require NLP-based Named Entity Recognition (NER). Regex cannot distinguish `"John Smith"` (a patient name) from a company name or product name without semantic context.
958
+
959
+ **What this means for HIPAA deployments:** HIPAA's Safe Harbor de-identification method lists 18 identifier categories that must be removed or replaced. FieldShield covers several — SSN, EMAIL, PHONE, DATE_OF_BIRTH, NPI, DEA — but does not cover names, geographic data below state level, or device identifiers. FieldShield is a defence-in-depth control for structured identifiers — it does not constitute complete HIPAA de-identification on its own.
960
+
961
+ Applications handling free-text clinical notes should implement server-side NER-based PHI detection in addition to FieldShield's client-side structured pattern detection.
962
+
963
+ ### PHI context-dependency
964
+
965
+ Some identifiers that appear individually non-sensitive become PHI when combined with other data in the same field. Two examples from FieldShield's pattern set:
966
+
967
+ **NPI numbers** are publicly searchable via the CMS NPPES registry — a provider's NPI alone is not sensitive. But `"Patient referred to NPI 1234567893 for oncology follow-up"` is a PHI-containing clinical note. FieldShield detects the NPI as a signal that the field likely contains a PHI combination.
968
+
969
+ **SWIFT/BIC codes** identify banks, not individuals. But a field containing `"Wire to DEUTDEDBBER account DE89370400440532013000"` is a sensitive financial record. FieldShield detects the SWIFT code as a signal that the field likely contains a wire transfer instruction.
970
+
971
+ The library's philosophy is that false negatives are worse than false positives in a security context — detecting a non-sensitive identifier that appears in a sensitive context is preferable to missing a sensitive combination entirely.
972
+
973
+ ## Versioning and pattern updates
974
+
975
+ FieldShield follows semantic versioning:
976
+
977
+ - **Patch** (`1.0.x`) — bug fixes, false positive/negative corrections to existing patterns
978
+ - **Minor** (`1.x.0`) — new patterns, new props, new features — backwards compatible
979
+ - **Major** (`x.0.0`) — breaking API changes
980
+
981
+ **Pattern updates are minor releases, not patches.** A new pattern could start flagging content in a field that was previously clean, which affects application behavior. Treat pattern updates as you would any minor dependency upgrade — review the CHANGELOG before updating.
982
+
983
+ **Pinning patterns** — if your application requires a frozen pattern set (e.g. for a compliance audit that was performed against a specific version), pin your FieldShield version explicitly:
984
+
985
+ ```json
986
+ "dependencies": {
987
+ "fieldshield": "1.0.4"
988
+ }
989
+ ```
990
+
991
+ See [CHANGELOG.md](./CHANGELOG.md) for a full history of pattern changes and API updates.
992
+
993
+ ---
994
+
995
+ ## TypeScript
996
+
997
+ All types are exported from the package root:
998
+
999
+ ```ts
1000
+ import type {
1001
+ FieldShieldHandle,
1002
+ FieldShieldInputProps,
1003
+ SensitiveClipboardEvent,
1004
+ CustomPattern,
1005
+ } from "fieldshield";
1006
+
1007
+ import type {
1008
+ SecurityEvent,
1009
+ SecurityEventType,
1010
+ UseSecurityLogOptions,
1011
+ UseSecurityLogReturn,
1012
+ } from "fieldshield";
1013
+
1014
+ import type { FieldShieldRefMap, SecureValues } from "fieldshield";
1015
+ ```
1016
+
1017
+ FieldShield is written in strict TypeScript. All public APIs are fully typed with no `any`.
1018
+
1019
+ ---
1020
+
1021
+ ## Compliance notes
1022
+
1023
+ ### HIPAA
1024
+
1025
+ FieldShield provides technical safeguards relevant to the HIPAA Security Rule (45 CFR § 164.312):
1026
+
1027
+ - **Access controls** — real values are only retrievable via `getSecureValue()`, not readable from the DOM
1028
+ - **Audit controls** — `useSecurityLog` provides structured, timestamped records of clipboard operations, form submissions, and memory purges
1029
+ - **Transmission security** — values never travel over `postMessage` broadcast channels; `MessageChannel` is point-to-point
1030
+
1031
+ FieldShield is a technical control, not a compliance attestation. It must be used as part of a broader HIPAA compliance program that includes administrative and physical safeguards.
1032
+
1033
+ ### PCI-DSS
1034
+
1035
+ FieldShield addresses PCI-DSS Requirement 6.4 (protect web-facing applications) by preventing cardholder data from appearing in the DOM where it could be captured by browser-based skimmers. The `CREDIT_CARD` pattern covers Visa, Mastercard, and Amex in all common formats.
1036
+
1037
+ ### SOC 2
1038
+
1039
+ The `PURGE` mechanism provides demonstrable evidence of data disposal. The `useSecurityLog` hook provides an audit trail that can be shipped to a backend logging service for SOC 2 Type II evidence collection.
1040
+
1041
+ ---
1042
+
1043
+ ## License
1044
+
1045
+ MIT