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,330 @@
1
+ /**
2
+ * @file FieldShieldInput.tsx
3
+ * @description FieldShield input component — protects sensitive text fields
4
+ * from browser extension DOM scraping, automated screen scrapers, and
5
+ * accidental clipboard exfiltration to LLMs.
6
+ *
7
+ * @example Standard mode
8
+ * ```tsx
9
+ * const ref = useRef<FieldShieldHandle>(null);
10
+ *
11
+ * const handleSubmit = async () => {
12
+ * const value = await ref.current?.getSecureValue();
13
+ * await fetch("/api/save", { body: JSON.stringify({ value }) });
14
+ * ref.current?.purge();
15
+ * };
16
+ *
17
+ * <FieldShieldInput
18
+ * ref={ref}
19
+ * label="Patient Notes"
20
+ * onSensitiveCopyAttempt={(e) =>
21
+ * toast.warning(`Blocked ${e.findings.join(", ")} from clipboard`)
22
+ * }
23
+ * />
24
+ * ```
25
+ *
26
+ * @example Textarea
27
+ * ```tsx
28
+ * <FieldShieldInput ref={ref} label="Clinical Notes" type="textarea" />
29
+ * ```
30
+ *
31
+ * @example Accessibility mode (WCAG 2.1 AA / Section 508)
32
+ * ```tsx
33
+ * <FieldShieldInput ref={ref} label="SSN" a11yMode />
34
+ * ```
35
+ *
36
+ * @module FieldShieldInput
37
+ */
38
+ import React from "react";
39
+ import { type CustomPattern } from "../hooks/useFieldShield";
40
+ import "../styles/fieldshield.css";
41
+ /**
42
+ * Imperative methods exposed to a parent component via `ref`.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * const ref = useRef<FieldShieldHandle>(null);
47
+ * <FieldShieldInput ref={ref} label="Notes" />
48
+ *
49
+ * // On submit:
50
+ * const value = await ref.current?.getSecureValue();
51
+ * ```
52
+ */
53
+ export interface FieldShieldHandle {
54
+ /**
55
+ * Retrieves the real, unmasked value from the worker's isolated memory.
56
+ * @see {@link UseFieldShieldReturn.getSecureValue}
57
+ */
58
+ getSecureValue: () => Promise<string>;
59
+ /**
60
+ * Zeros out the stored value in worker memory.
61
+ * @see {@link UseFieldShieldReturn.purge}
62
+ */
63
+ purge: () => void;
64
+ }
65
+ /**
66
+ * Payload delivered to `onSensitiveCopyAttempt` and `onSensitivePaste`
67
+ * callbacks whenever a clipboard event involves sensitive data.
68
+ */
69
+ export interface SensitiveClipboardEvent {
70
+ /** ISO 8601 timestamp of when the event occurred. */
71
+ timestamp: string;
72
+ /** The `label` prop of the field that triggered the event. */
73
+ fieldLabel: string;
74
+ /**
75
+ * Pattern names that were active at the time of the event.
76
+ * Example: `["SSN", "PHONE"]`
77
+ */
78
+ findings: string[];
79
+ /**
80
+ * The masked text written to (copy/cut) or read from (paste) the clipboard.
81
+ * Sensitive spans replaced by `█`. The real value is never included here.
82
+ */
83
+ masked: string;
84
+ /** Whether the event originated from a copy, cut, or paste action. */
85
+ eventType: "copy" | "cut" | "paste";
86
+ }
87
+ /**
88
+ * Props accepted by {@link FieldShieldInput}.
89
+ */
90
+ export interface FieldShieldInputProps {
91
+ /**
92
+ * Visible label text linked to the input via `htmlFor`/`id`.
93
+ * Also used as the field identifier in clipboard event payloads.
94
+ * When omitted, no `<label>` element is rendered — the field falls back
95
+ * to `"Protected field"` for screen reader announcements.
96
+ */
97
+ label?: string;
98
+ /**
99
+ * Renders either a single-line `<input>` or a multi-line `<textarea>`.
100
+ * Textarea mode also enables auto-grow behaviour — the field expands
101
+ * vertically as the user types past the initial height.
102
+ *
103
+ * @defaultValue "text"
104
+ */
105
+ type?: "text" | "textarea";
106
+ /** Native `placeholder` attribute forwarded to the underlying element. */
107
+ placeholder?: string;
108
+ /**
109
+ * Additional sensitive-data patterns to layer on top of the built-in
110
+ * defaults. See `FIELDSHIELD_PATTERNS` for the full list. Opt-in patterns
111
+ * from `OPT_IN_PATTERNS` can be added here when the field context warrants them.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * customPatterns={[{ name: "EMPLOYEE_ID", regex: "EMP-\\d{6}" }]}
116
+ * ```
117
+ */
118
+ customPatterns?: CustomPattern[];
119
+ /**
120
+ * Additional CSS class applied to the outermost container `<div>`.
121
+ * Merged with the internal `fieldshield-container` class.
122
+ */
123
+ className?: string;
124
+ /** Inline styles applied to the outermost container `<div>`. */
125
+ style?: React.CSSProperties;
126
+ /**
127
+ * Fires whenever the masked value or findings change — after each Worker
128
+ * UPDATE response. Gives the parent visibility into field state WITHOUT
129
+ * exposing the real value. The real value is only available via
130
+ * `ref.current.getSecureValue()`.
131
+ *
132
+ * @param masked - Current masked display string (e.g. `"SSN: ███-██-████"`).
133
+ * @param findings - Deduplicated list of matched pattern names.
134
+ */
135
+ onChange?: (masked: string, findings: string[]) => void;
136
+ /**
137
+ * When `true`, disables DOM scrambling and renders a native
138
+ * `type="password"` input instead. Pattern detection and clipboard
139
+ * protection remain active.
140
+ *
141
+ * Use this mode for WCAG 2.1 AA / Section 508 compliance — screen readers
142
+ * handle `type="password"` fields natively and cannot interact with the
143
+ * scrambled overlay used in standard mode.
144
+ *
145
+ * @defaultValue false
146
+ */
147
+ a11yMode?: boolean;
148
+ /**
149
+ * Fired when the user copies or cuts from the field while sensitive
150
+ * patterns are present. The clipboard receives the masked text instead
151
+ * of the real value.
152
+ *
153
+ * Use this to surface a toast notification or write a security audit log.
154
+ *
155
+ * @param event - Details of the blocked clipboard operation.
156
+ */
157
+ onSensitiveCopyAttempt?: (event: SensitiveClipboardEvent) => void;
158
+ /**
159
+ * Fired when the user pastes content into the field that contains sensitive
160
+ * patterns.
161
+ *
162
+ * Return `false` to block the paste entirely — the field reverts to its
163
+ * previous value and the clipboard content is discarded. Return nothing
164
+ * (or `true`) to allow the paste to proceed.
165
+ *
166
+ * Use the block behavior for fields where sensitive data should never be
167
+ * pasted — for example a "reason for visit" field that should not accept
168
+ * SSNs. Use the allow behavior (default) for fields where the user is
169
+ * legitimately entering their own sensitive data.
170
+ *
171
+ * @param event - Details of the detected paste content.
172
+ * @returns `false` to block the paste, `void` or `true` to allow it.
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * // Block sensitive pastes
177
+ * onSensitivePaste={(e) => {
178
+ * logAuditEvent(e);
179
+ * return false; // revert the paste
180
+ * }}
181
+ *
182
+ * // Allow sensitive pastes but log them
183
+ * onSensitivePaste={(e) => {
184
+ * logAuditEvent(e);
185
+ * // return nothing — paste proceeds
186
+ * }}
187
+ * ```
188
+ */
189
+ onSensitivePaste?: (event: SensitiveClipboardEvent) => boolean | void;
190
+ /**
191
+ * Fired when the field receives focus. Forwarded directly from the
192
+ * underlying `<input>` or `<textarea>` element.
193
+ */
194
+ onFocus?: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
195
+ /**
196
+ * Fired when the field loses focus. Forwarded directly from the
197
+ * underlying `<input>` or `<textarea>` element.
198
+ */
199
+ onBlur?: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
200
+ /**
201
+ * Disables the field when `true`. Forwarded to the underlying element and
202
+ * reflected on the container via the `data-disabled` attribute for CSS
203
+ * styling hooks.
204
+ *
205
+ * @defaultValue false
206
+ */
207
+ disabled?: boolean;
208
+ /**
209
+ * Marks the field as required. Sets `aria-required` on the underlying
210
+ * element so screen readers announce the field as mandatory.
211
+ *
212
+ * @defaultValue false
213
+ */
214
+ required?: boolean;
215
+ /**
216
+ * Maximum number of characters the field will accept. Forwarded to the
217
+ * underlying element's native `maxLength` attribute. Also caps the amount
218
+ * of text the worker processes on each keystroke.
219
+ */
220
+ maxLength?: number;
221
+ /**
222
+ * Maximum number of characters sent to the worker for pattern detection.
223
+ * If the user types or pastes beyond this limit the input is blocked —
224
+ * the field reverts to its previous value and `onMaxLengthExceeded` fires.
225
+ *
226
+ * Blocking rather than truncating is intentional: truncation would create
227
+ * a blind spot where sensitive data beyond the limit is never scanned.
228
+ *
229
+ * This is distinct from `maxLength` which restricts the HTML input at the
230
+ * browser level. Use `maxLength` for structured fields with known lengths
231
+ * (SSN, credit card). Use `maxProcessLength` to cap worker processing for
232
+ * free-text fields where longer input is valid but should be bounded.
233
+ *
234
+ * @defaultValue 100000
235
+ */
236
+ maxProcessLength?: number;
237
+ /**
238
+ * Called when input is blocked because it exceeds `maxProcessLength`.
239
+ * Use this to surface a character count warning or error message to the user.
240
+ *
241
+ * @param length - The actual input length that triggered the block.
242
+ * @param limit - The `maxProcessLength` limit that was exceeded.
243
+ *
244
+ * @example
245
+ * ```tsx
246
+ * onMaxLengthExceeded={(length, limit) =>
247
+ * setError(`Input too long — maximum ${limit} characters allowed`)
248
+ * }
249
+ * ```
250
+ */
251
+ onMaxLengthExceeded?: (length: number, limit: number) => void;
252
+ /**
253
+ * Called when the Web Worker encounters a runtime error.
254
+ *
255
+ * When this fires, FieldShieldInput has already reset `masked` and
256
+ * `findings` to empty so the field does not freeze showing stale warnings.
257
+ * The worker is NOT terminated — a transient error may not affect subsequent
258
+ * messages. If the error is persistent, consider displaying a warning and
259
+ * asking the user to refresh.
260
+ *
261
+ * If the worker fails to initialize entirely (e.g. due to a strict CSP),
262
+ * the component automatically falls back to `a11yMode` — this callback is
263
+ * NOT called in that case. Listen for the `a11yMode` fallback by providing
264
+ * `onWorkerError` and checking whether `e.message` contains "initialize".
265
+ *
266
+ * @param error - The ErrorEvent from the worker's onerror handler.
267
+ *
268
+ * @example
269
+ * ```tsx
270
+ * onWorkerError={(e) =>
271
+ * console.error("FieldShield worker error:", e.message)
272
+ * }
273
+ * ```
274
+ */
275
+ onWorkerError?: (error: ErrorEvent) => void;
276
+ /**
277
+ * Initial number of visible text rows. Only applies when `type="textarea"`.
278
+ * Sets a minimum height — the field still auto-grows beyond this value as
279
+ * the user types.
280
+ *
281
+ * @defaultValue 3
282
+ */
283
+ rows?: number;
284
+ /**
285
+ * Hint to the browser about which virtual keyboard to display on mobile.
286
+ * Does not affect input behaviour or value handling — the field always
287
+ * operates as `type="text"` internally, preserving scrambling and worker
288
+ * isolation regardless of this value.
289
+ *
290
+ * Use this instead of `type="number"` or `type="email"` — those change
291
+ * browser validation and value parsing in ways that break DOM scrambling.
292
+ * `inputMode` gives the correct mobile keyboard without any side effects.
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * // Numeric keypad for SSN / credit card fields
297
+ * <FieldShieldInput inputMode="numeric" label="SSN" />
298
+ *
299
+ * // Phone keypad with +, *, # keys
300
+ * <FieldShieldInput inputMode="tel" label="Phone" />
301
+ *
302
+ * // Email keyboard with @ key prominent
303
+ * <FieldShieldInput inputMode="email" label="Email" />
304
+ * ```
305
+ *
306
+ * @defaultValue "text"
307
+ */
308
+ inputMode?: "text" | "numeric" | "decimal" | "tel" | "email" | "search" | "url" | "none";
309
+ }
310
+ /**
311
+ * A drop-in replacement for `<input>` or `<textarea>` in contexts where
312
+ * sensitive data is typed or pasted by users.
313
+ *
314
+ * **Threat model (what this protects against)**
315
+ * - Browser extensions reading `input.value` via DOM inspection
316
+ * - Session recording tools (FullStory, LogRocket) capturing field content
317
+ * - Automated scrapers walking the DOM
318
+ * - Users accidentally copying sensitive text into LLMs via clipboard
319
+ *
320
+ * **Out of scope**
321
+ * - Extensions with kernel-level or debugger access
322
+ * - OS-level keyloggers
323
+ * - Network interception (use TLS)
324
+ *
325
+ * @remarks
326
+ * Uses `forwardRef` so parent components can hold a {@link FieldShieldHandle}
327
+ * ref and call `getSecureValue()` on form submission without maintaining a
328
+ * separate copy of the real value on the main thread.
329
+ */
330
+ export declare const FieldShieldInput: React.ForwardRefExoticComponent<FieldShieldInputProps & React.RefAttributes<FieldShieldHandle>>;