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/CHANGELOG.md +101 -0
- package/LICENSE +21 -0
- package/README.md +1045 -0
- package/THREAT_MODEL.md +360 -0
- package/dist/assets/fieldshield.css +1 -0
- package/dist/assets/fieldshield.worker.js +1 -0
- package/dist/components/FieldShieldInput.d.ts +330 -0
- package/dist/fieldshield.js +790 -0
- package/dist/fieldshield.umd.cjs +4 -0
- package/dist/hooks/useFieldShield.d.ts +157 -0
- package/dist/hooks/useSecurityLog.d.ts +139 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/patterns.d.ts +105 -0
- package/dist/utils/collectSecureValue.d.ts +102 -0
- package/package.json +107 -0
|
@@ -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>>;
|