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,4 @@
1
+ (function(f,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],t):(f=typeof globalThis<"u"?globalThis:f||self,t(f.FieldShield={},f.ReactJSXRuntime,f.React))})(this,(function(f,t,s){"use strict";var B=typeof document<"u"?document.currentScript:null;const j=Object.freeze({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})",AWS_ACCESS_KEY:"\\b(AKIA|ASIA)[0-9A-Z]{16}\\b",SSN:"\\b\\d{3}[-\\s.]?\\d{2}[-\\s.]?\\d{4}\\b",EMAIL:"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",PHONE:"\\b(?:\\+?1[-. ]?)?\\(?[2-9]\\d{2}\\)?[-. ]?\\d{3}[-. ]?\\d{4}\\b|\\+[1-9][\\s.-]?(?:\\d[\\s.-]?){6,14}\\d\\b",CREDIT_CARD:["\\b4\\d{3}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b","\\b5[1-5]\\d{2}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b","\\b3[47]\\d{2}[-\\s]?\\d{6}[-\\s]?\\d{5}\\b"].join("|"),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",TAX_ID:"\\b\\d{2}-\\d{7}\\b|\\b\\d{9}\\b",GITHUB_TOKEN:"\\b(ghp|gho|ghs|ghu|github_pat)_[a-zA-Z0-9_]{20,}\\b",STRIPE_KEY:"\\b(sk|pk|rk)_(live|test)_[a-zA-Z0-9]{20,}\\b",JWT:"\\beyJ[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\b",PRIVATE_KEY_BLOCK:"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",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"}),ce=Object.freeze({IBAN:"\\b[A-Z]{2}\\d{2}[-\\s]?(?:[A-Z0-9]{1,4}[-\\s]?){3,}[A-Z0-9]{1,4}\\b",DEA_NUMBER:"\\b[ABCDEFGHJKLMPRSTUX][A-Z]\\d{7}\\b",SWIFT_BIC:"\\b[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?\\b",NPI_NUMBER:"\\b[12]\\d{9}\\b",PASSPORT_NUMBER:"\\b[A-Z]{1,2}[0-9]{6,9}\\b"}),ee=3e3,oe=1e5,te=d=>Object.fromEntries(d.map(l=>[l.name,l.regex])),se=(d=[],l=oe,A,b)=>{const[y,S]=s.useState(""),[R,$]=s.useState([]),[P,v]=s.useState(!1),r=s.useRef(null),w=JSON.stringify(d);s.useEffect(()=>{let m=!1;try{r.current=new Worker(new URL("/assets/fieldshield.worker.js",typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:B&&B.tagName.toUpperCase()==="SCRIPT"&&B.src||new URL("fieldshield.umd.cjs",document.baseURI).href),{type:"module"})}catch(c){console.error("[FieldShield] Worker failed to initialize — falling back to a11yMode.",c),queueMicrotask(()=>v(!0));return}return r.current.onmessage=c=>{m||c.data?.type==="UPDATE"&&typeof c.data.masked=="string"&&Array.isArray(c.data.findings)&&(S(c.data.masked),$(c.data.findings))},r.current.onerror=c=>{m||(console.error(`[FieldShield] Worker runtime error: ${c.message??"unknown error"}`),S(""),$([]),b?.(c))},r.current.postMessage({type:"CONFIG",payload:{defaultPatterns:j,customPatterns:te([])}}),()=>{m=!0,r.current?.terminate(),r.current=null}},[]),s.useEffect(()=>{r.current&&r.current.postMessage({type:"CONFIG",payload:{defaultPatterns:j,customPatterns:te(d)}})},[w]);const D=s.useCallback(m=>m.length>l?(console.warn(`[FieldShield] Input length ${m.length} exceeds maxProcessLength (${l}). Keystroke blocked to prevent unprotected data beyond the limit. Raise maxProcessLength if this field requires longer input.`),A?.(m.length,l),!1):(r.current?.postMessage({type:"PROCESS",payload:{text:m}}),!0),[l,A]),k=s.useCallback(()=>new Promise((m,c)=>{if(!r.current){m("");return}const{port1:I,port2:V}=new MessageChannel,G=setTimeout(()=>{I.close(),c(new Error(`[FieldShield] getSecureValue timed out after ${ee}ms.`))},ee);I.onmessage=W=>{clearTimeout(G),I.close(),m(W.data.text)},r.current.postMessage({type:"GET_TRUTH"},[V])}),[]),x=s.useCallback(()=>{r.current?.postMessage({type:"PURGE"})},[]);return{masked:y,findings:R,processText:D,getSecureValue:k,purge:x,workerFailed:P}},ae=s.forwardRef(({label:d,type:l="text",placeholder:A,customPatterns:b=[],className:y,style:S,onChange:R,a11yMode:$=!1,onSensitiveCopyAttempt:P,onSensitivePaste:v,onFocus:r,onBlur:w,disabled:D=!1,required:k=!1,maxLength:x,rows:m=3,inputMode:c="text",maxProcessLength:I=1e5,onMaxLengthExceeded:V,onWorkerError:G},W)=>{const{masked:K,findings:Z,processText:z,getSecureValue:ne,purge:re,workerFailed:he}=se(b,I,V,G),ge=$||he,u=Z.length>0,F=d??"Protected field",J=s.useRef(null),le=s.useRef(null),L=s.useRef(null),p=s.useRef(""),_=s.useId(),N=`${_}-warning`,H=`${_}-desc`;s.useImperativeHandle(W,()=>({getSecureValue:ne,purge:()=>{re(),z(""),p.current="";const e=J.current??le.current;e&&(e.value="",L.current&&(L.current.textContent=`
2
+ `))}}),[ne,re,z]),s.useEffect(()=>{R?.(K,Z)},[K,Z,R]);const Y=(e,a,n)=>{if(!z(a)){const E=p.current.replace(/[^\n]/g,"x");e.value=E;const g=Math.min(n,p.current.length);e.setSelectionRange(g,g);return}p.current=a;const i=a.replace(/[^\n]/g,"x");e.value=i,e.setSelectionRange(n,n),L.current&&(L.current.textContent=i+`
3
+ `)},ie=e=>{const a=e.target,n=a.value,h=a.selectionStart??n.length,i=p.current,E=n.length-i.length;let g;if(E>0){const o=Math.max(0,h-E),T=n.slice(o,h);g=i.slice(0,o)+T+i.slice(o)}else if(E<0){const o=h,T=o-E;g=i.slice(0,o)+i.slice(T)}else{let o=-1,T=-1;for(let C=0;C<n.length;C++)n[C]!=="x"&&n[C]!==`
4
+ `&&(o===-1&&(o=C),T=C+1);if(o!==-1){const C=n.slice(o,T);g=i.slice(0,o)+C+i.slice(T)}else g=i}Y(a,g,h)},be=e=>{const a=e.target.value;p.current=a,z(a)},X=e=>{e.preventDefault();const a=e.clipboardData.getData("text/plain"),n=e.target,h=n.selectionStart??0,i=n.selectionEnd??0,E=p.current,g=E.length-(i-h)+a.length;if(g>I){console.warn(`[FieldShield] Paste blocked — result length ${g} would exceed maxProcessLength (${I}). DOM unchanged.`),V?.(g,I);return}const o=E.slice(0,h)+a+E.slice(i),T=h+a.length;if(Y(n,o,T),v&&a){const C=[...Object.entries(j),...b.map(O=>[O.name,O.regex])],de=[];for(const[O,U]of C)try{de.push([O,new RegExp(U,"gi")])}catch{}const q=[];let Q=a;for(const[O,U]of de)U.lastIndex=0,U.test(a)&&(q.push(O),U.lastIndex=0,Q=Q.replace(U,Se=>"█".repeat(Se.length)));q.length>0&&v({timestamp:new Date().toISOString(),fieldLabel:F,findings:[...new Set(q)],masked:Q,eventType:"paste"})===!1&&Y(n,E,h)}},M=e=>{e.preventDefault();const a=e.target,n=a.selectionStart??0,h=a.selectionEnd??p.current.length,i=K.slice(n,h),E=i.includes("█");if(u&&E?(e.clipboardData.setData("text/plain",i),P?.({timestamp:new Date().toISOString(),fieldLabel:F,findings:[...Z],masked:i,eventType:e.type==="cut"?"cut":"copy"})):e.clipboardData.setData("text/plain",p.current.slice(n,h)),e.type==="cut"){const g=p.current.slice(0,n),o=p.current.slice(h);p.current=g+o,z(p.current);const T="x".repeat(p.current.length);a.value=T,a.setSelectionRange(n,n)}};return ge?t.jsxs("div",{className:`fieldshield-container${y?` ${y}`:""}`,style:S,role:"group","aria-labelledby":_,"data-disabled":D||void 0,children:[d&&t.jsx("label",{htmlFor:_,className:"fieldshield-label",children:d}),t.jsx("span",{id:H,className:"fieldshield-sr-only",children:"This field is protected. Sensitive data patterns will be detected and blocked from copying."}),t.jsx("input",{id:_,ref:J,type:"password",className:"fieldshield-a11y-input",placeholder:A,onChange:be,onCopy:M,onCut:M,onPaste:X,onFocus:r,onBlur:w,disabled:D,required:k,maxLength:x,inputMode:c,spellCheck:!1,autoComplete:"off","aria-required":k,"aria-label":d?void 0:F,"aria-describedby":`${H} ${u?N:""}`.trim(),"aria-invalid":u?"true":"false","aria-errormessage":u?N:void 0}),t.jsx("div",{id:N,role:"status","aria-live":"polite","aria-atomic":"true",className:"fieldshield-findings",children:u&&t.jsxs(t.Fragment,{children:[t.jsx("span",{className:"fieldshield-warning-icon","aria-hidden":"true",children:"⚠"}),t.jsxs("span",{className:"fieldshield-warning-text",children:["Sensitive data detected. Clipboard blocked for:"," "]}),Z.map(e=>t.jsx("span",{className:"fieldshield-tag","aria-label":`pattern: ${e}`,children:e},e))]})})]}):t.jsxs("div",{className:`fieldshield-container${y?` ${y}`:""}`,style:S,role:"group","aria-labelledby":_,"data-disabled":D||void 0,children:[d&&t.jsx("label",{htmlFor:_,className:"fieldshield-label",children:d}),t.jsx("span",{id:H,className:"fieldshield-sr-only",children:"Sensitive field. Input is protected. Sensitive data patterns will be detected and blocked from copying."}),t.jsxs("div",{className:"fieldshield-field-wrapper",children:[t.jsx("div",{className:`fieldshield-mask-layer${u?" fieldshield-mask-unsafe":""}`,"aria-hidden":"true",children:K||t.jsx("span",{className:"fieldshield-placeholder",children:A})}),l==="textarea"&&t.jsx("div",{ref:L,className:"fieldshield-grow","aria-hidden":"true"}),l==="textarea"?t.jsx("textarea",{ref:le,id:_,className:"fieldshield-real-input",placeholder:A,onChange:ie,onPaste:X,onCopy:M,onCut:M,onFocus:r,onBlur:w,disabled:D,required:k,maxLength:x,rows:m,inputMode:c,spellCheck:!1,autoComplete:"off","aria-required":k,"aria-label":u?`${F} — sensitive data detected`:`${F} — protected input`,"aria-describedby":`${H} ${u?N:""}`.trim(),"aria-invalid":u?"true":"false","aria-errormessage":u?N:void 0}):t.jsx("input",{ref:J,id:_,type:"text",className:"fieldshield-real-input",placeholder:A,onChange:ie,onPaste:X,onCopy:M,onCut:M,onFocus:r,onBlur:w,disabled:D,required:k,maxLength:x,inputMode:c,spellCheck:!1,autoComplete:"off","aria-required":k,"aria-label":u?`${F} — sensitive data detected`:`${F} — protected input`,"aria-describedby":`${H} ${u?N:""}`.trim(),"aria-invalid":u?"true":"false","aria-errormessage":u?N:void 0})]}),t.jsx("div",{id:N,role:"status","aria-live":"polite","aria-atomic":"true",className:"fieldshield-findings",children:u&&t.jsxs(t.Fragment,{children:[t.jsx("span",{className:"fieldshield-warning-icon","aria-hidden":"true",children:"⚠"}),t.jsxs("span",{className:"fieldshield-warning-text",children:["Sensitive data detected. Clipboard blocked for:"," "]}),Z.map(e=>t.jsx("span",{className:"fieldshield-tag","aria-label":`pattern: ${e}`,children:e},e))]})})]})});ae.displayName="FieldShieldInput";const ue=(d={})=>{const{maxEvents:l=20}=d,[A,b]=s.useState([]),y=s.useRef(0),S=s.useCallback(P=>{const v=++y.current,r=new Date().toLocaleTimeString();b(w=>[{...P,id:v,timestamp:r},...w].slice(0,l))},[l]),R=s.useCallback(P=>v=>{const r=P==="paste"?"PASTE_DETECTED":v.eventType==="cut"?"CUT_BLOCKED":"COPY_BLOCKED";S({field:v.fieldLabel,type:r,findings:v.findings,detail:`${v.masked.slice(0,32)}${v.masked.length>32?"…":""}`})},[S]),$=s.useCallback(()=>{b([]),y.current=0},[]);return{events:A,pushEvent:S,makeClipboardHandler:R,clearLog:$}},fe=async d=>{const l=Object.entries(d),A=await Promise.allSettled(l.map(([,b])=>b.current?.getSecureValue()??Promise.resolve("")));return Object.fromEntries(l.map(([b],y)=>{const S=A[y];return S.status==="rejected"?(console.warn(`[FieldShield] collectSecureValues: field "${String(b)}" failed to retrieve value.`,S.reason),[b,""]):[b,S.value]}))},pe=d=>{Object.values(d).forEach(l=>l.current?.purge())};f.FIELDSHIELD_PATTERNS=j,f.FieldShieldInput=ae,f.OPT_IN_PATTERNS=ce,f.collectSecureValues=fe,f.purgeSecureValues=pe,f.useFieldShield=se,f.useSecurityLog=ue,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @file useFieldShield.ts
3
+ * @description React hook that manages the lifecycle of the FieldShield Web Worker,
4
+ * exposes masked output and pattern findings for rendering, and provides
5
+ * imperative methods for secure value retrieval and memory purging.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const { masked, findings, processText, getSecureValue, purge } =
10
+ * useFieldShield([{ name: "EMPLOYEE_ID", regex: "EMP-\\d{6}" }]);
11
+ * ```
12
+ *
13
+ * @module useFieldShield
14
+ */
15
+ /**
16
+ * A custom sensitive-data pattern supplied by the consuming application.
17
+ *
18
+ * @remarks
19
+ * The public API intentionally uses an array of objects rather than a plain
20
+ * Record for two reasons:
21
+ *
22
+ * 1. **Order is preserved** — arrays have guaranteed iteration order. Developers
23
+ * who want to prioritise certain patterns can control the order they are
24
+ * applied.
25
+ * 2. **Duplicate detection** — two entries with the same `name` are visible in
26
+ * an array and can be warned about. In a Record the second key silently
27
+ * overwrites the first.
28
+ *
29
+ * The array is converted to a `Record<string, string>` by {@link toPatternRecord}
30
+ * before being sent to the worker, so the worker remains agnostic to the
31
+ * public API shape.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * { name: "EMPLOYEE_ID", regex: "EMP-\\d{6}" }
36
+ * ```
37
+ */
38
+ export interface CustomPattern {
39
+ /** Human-readable label shown in the findings list (e.g. `"EMPLOYEE_ID"`). */
40
+ name: string;
41
+ /**
42
+ * Regular expression source string. Do **not** include delimiters (`/`) or
43
+ * flags — the worker applies `gi` automatically.
44
+ *
45
+ * Use double backslashes for escape sequences since this is a string, not a
46
+ * regex literal. For example: `"\\d{6}"` not `"\d{6}"`.
47
+ */
48
+ regex: string;
49
+ }
50
+ /**
51
+ * Return value of {@link useFieldShield}.
52
+ */
53
+ export interface UseFieldShieldReturn {
54
+ /**
55
+ * A copy of the current input value with sensitive spans replaced by
56
+ * `█` characters. Safe to render directly in the UI.
57
+ */
58
+ masked: string;
59
+ /**
60
+ * Deduplicated list of pattern names that matched the current value
61
+ * (e.g. `["SSN", "EMAIL"]`). Empty array when the field is clean.
62
+ */
63
+ findings: string[];
64
+ /**
65
+ * Send the current real value to the worker for pattern analysis.
66
+ * Call this on every input change.
67
+ *
68
+ * Returns `true` if the input was accepted and sent to the worker,
69
+ * or `false` if it was blocked because it exceeded `maxProcessLength`.
70
+ * The caller (e.g. `handleChange` in `FieldShieldInput`) must revert
71
+ * the DOM to the previous value when `false` is returned.
72
+ *
73
+ * @param text - The unmasked text to evaluate.
74
+ * @returns `true` if accepted, `false` if blocked.
75
+ */
76
+ processText: (text: string) => boolean;
77
+ /**
78
+ * Retrieve the real, unmasked value from the worker's isolated memory
79
+ * via a private `MessageChannel`. Use this on form submission rather than
80
+ * maintaining a separate copy of the real value in the main thread.
81
+ *
82
+ * @returns A promise that resolves to the current unmasked value, or
83
+ * rejects with a timeout error if the worker does not respond within
84
+ * 3 seconds.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const handleSubmit = async () => {
89
+ * const realValue = await getSecureValue();
90
+ * await fetch("/api/save", { body: JSON.stringify({ value: realValue }) });
91
+ * };
92
+ * ```
93
+ */
94
+ getSecureValue: () => Promise<string>;
95
+ /**
96
+ * Zero out the stored value in the worker's memory and confirm deletion.
97
+ * Call this after successful form submission or on session logout.
98
+ *
99
+ * @remarks
100
+ * Useful for compliance environments (HIPAA, PCI-DSS, SOC 2) that require
101
+ * demonstrable cleanup of sensitive data from application memory.
102
+ */
103
+ purge: () => void;
104
+ /**
105
+ * `true` if the Web Worker failed to initialize and the hook has fallen
106
+ * back to no-op mode. The consuming component should switch to `a11yMode`
107
+ * when this is `true` to ensure the field remains usable.
108
+ *
109
+ * Common causes: strict CSP blocking worker initialization, unsupported
110
+ * browser context (some sandboxed iframes), or browser memory pressure.
111
+ */
112
+ workerFailed: boolean;
113
+ }
114
+ /**
115
+ * Default maximum number of characters sent to the worker for processing.
116
+ * Large enough for legitimate clinical notes and free-text fields while
117
+ * protecting against denial-of-service via adversarially crafted input.
118
+ * Consumers can raise or lower this via the `maxProcessLength` parameter.
119
+ */
120
+ export declare const DEFAULT_MAX_PROCESS_LENGTH = 100000;
121
+ /**
122
+ * Manages a {@link fieldshield.worker | FieldShield Web Worker} for the lifetime of
123
+ * the consuming component, keeping the worker alive across pattern updates to
124
+ * avoid losing `internalTruth`.
125
+ *
126
+ * @param customPatterns - Optional additional patterns to layer on top of the
127
+ * built-in defaults. Changing this array reconfigures the worker without
128
+ * terminating it, preserving any value already in memory.
129
+ *
130
+ * @returns {@link UseFieldShieldReturn}
131
+ *
132
+ * @remarks
133
+ * **Two-effect design** — worker creation and pattern configuration are
134
+ * intentionally split into separate `useEffect` calls:
135
+ *
136
+ * - Effect 1 (`[]` deps): Creates the worker once on mount, immediately sends
137
+ * the built-in default patterns from `patterns.ts` via CONFIG, and terminates
138
+ * only on unmount. The worker's `internalTruth` survives prop changes.
139
+ * - Effect 2 (`[patternsString]` deps): Sends an updated CONFIG message
140
+ * whenever `customPatterns` changes. Sends both default and custom patterns
141
+ * together so the worker always has the full active set. No teardown occurs,
142
+ * so stored values are preserved.
143
+ *
144
+ * Combining these into one effect would destroy and recreate the worker —
145
+ * and its stored `internalTruth` — on every pattern update.
146
+ *
147
+ * **Cancelled flag** — a boolean closed over by the `onmessage` handler
148
+ * guards against stale worker responses arriving after the component unmounts.
149
+ * The cleanup function flips it to `true` before terminating the worker. Any
150
+ * message that arrives after that point is discarded, preventing a state update
151
+ * on an unmounted component.
152
+ *
153
+ * React 18 no longer warns on unmounted state updates, but the race condition
154
+ * still exists. The flag makes the hook correct by definition rather than
155
+ * correct by luck.
156
+ */
157
+ export declare const useFieldShield: (customPatterns?: CustomPattern[], maxProcessLength?: number, onMaxLengthExceeded?: (length: number, limit: number) => void, onWorkerError?: (error: ErrorEvent) => void) => UseFieldShieldReturn;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @file useSecurityLog.ts
3
+ * @description Hook that maintains a typed, auto-timestamped log of FieldShield
4
+ * security events — copy blocks, cut blocks, paste detections, submissions,
5
+ * and memory purges.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const { events, makeClipboardHandler, pushEvent, clearLog } = useSecurityLog();
10
+ *
11
+ * <FieldShieldInput
12
+ * label="SSN"
13
+ * onSensitiveCopyAttempt={makeClipboardHandler("copy_cut")}
14
+ * onSensitivePaste={makeClipboardHandler("paste")}
15
+ * />
16
+ *
17
+ * <ol>
18
+ * {events.map(ev => <li key={ev.id}>{ev.field} — {ev.type}</li>)}
19
+ * </ol>
20
+ * ```
21
+ *
22
+ * @module useSecurityLog
23
+ */
24
+ import type { SensitiveClipboardEvent } from "../components/FieldShieldInput";
25
+ /**
26
+ * Discriminated union of all event types the log can record.
27
+ *
28
+ * - `COPY_BLOCKED` — user attempted to copy sensitive data; clipboard received masked text.
29
+ * - `CUT_BLOCKED` — user attempted to cut sensitive data; clipboard received masked text.
30
+ * - `PASTE_DETECTED` — user pasted content containing sensitive patterns.
31
+ * - `SUBMIT` — form was submitted via `collectSecureValues`.
32
+ * - `PURGE` — worker memory was zeroed after submission.
33
+ * - `CUSTOM` — application-defined event pushed via `pushEvent` directly.
34
+ */
35
+ export type SecurityEventType = "COPY_BLOCKED" | "CUT_BLOCKED" | "PASTE_DETECTED" | "SUBMIT" | "PURGE" | "CUSTOM";
36
+ /**
37
+ * A single entry in the security event log.
38
+ */
39
+ export interface SecurityEvent {
40
+ /** Auto-incrementing unique identifier — safe to use as a React key. */
41
+ id: number;
42
+ /**
43
+ * Human-readable time string derived from the event timestamp.
44
+ * Formatted via `Date.toLocaleTimeString()` at the moment the event is pushed.
45
+ */
46
+ timestamp: string;
47
+ /**
48
+ * The field that produced the event. Sourced from `SensitiveClipboardEvent.fieldLabel`
49
+ * for clipboard events, or supplied directly for SUBMIT / PURGE events.
50
+ */
51
+ field: string;
52
+ /** Discriminated event type. See {@link SecurityEventType}. */
53
+ type: SecurityEventType;
54
+ /**
55
+ * Pattern names active at the time of the event (e.g. `["SSN", "EMAIL"]`).
56
+ * Empty array for SUBMIT, PURGE, and CUSTOM events that carry no findings.
57
+ */
58
+ findings: string[];
59
+ /**
60
+ * Optional human-readable detail string for display in the log UI.
61
+ * For clipboard events this is a truncated preview of the masked value.
62
+ */
63
+ detail?: string;
64
+ }
65
+ /**
66
+ * Options accepted by {@link useSecurityLog}.
67
+ */
68
+ export interface UseSecurityLogOptions {
69
+ /**
70
+ * Maximum number of events retained in the log. Oldest events are dropped
71
+ * when the limit is exceeded.
72
+ *
73
+ * @defaultValue 20
74
+ */
75
+ maxEvents?: number;
76
+ }
77
+ /**
78
+ * Return value of {@link useSecurityLog}.
79
+ */
80
+ export interface UseSecurityLogReturn {
81
+ /**
82
+ * The current list of security events, newest first.
83
+ * Safe to map directly as React children — each entry has a stable `id`.
84
+ */
85
+ events: SecurityEvent[];
86
+ /**
87
+ * Push any event into the log manually. Use this for SUBMIT and PURGE events
88
+ * that are not produced by a clipboard callback.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * pushEvent({ field: "All fields", type: "SUBMIT", findings: [], detail: "3 fields submitted" });
93
+ * ```
94
+ */
95
+ pushEvent: (event: Omit<SecurityEvent, "id" | "timestamp">) => void;
96
+ /**
97
+ * Returns a `SensitiveClipboardEvent` handler ready to wire directly into
98
+ * `onSensitiveCopyAttempt` or `onSensitivePaste`.
99
+ *
100
+ * Pass `"copy_cut"` for `onSensitiveCopyAttempt` — the handler inspects
101
+ * `e.eventType` internally to distinguish copy from cut.
102
+ * Pass `"paste"` for `onSensitivePaste`.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * <FieldShieldInput
107
+ * onSensitiveCopyAttempt={makeClipboardHandler("copy_cut")}
108
+ * onSensitivePaste={makeClipboardHandler("paste")}
109
+ * />
110
+ * ```
111
+ */
112
+ makeClipboardHandler: (context: "copy_cut" | "paste") => (e: SensitiveClipboardEvent) => void;
113
+ /**
114
+ * Clears all events from the log, resetting it to an empty state.
115
+ * Resets the internal ID counter.
116
+ */
117
+ clearLog: () => void;
118
+ }
119
+ /**
120
+ * Maintains a capped, auto-timestamped log of FieldShield security events.
121
+ *
122
+ * @param options - Optional configuration. See {@link UseSecurityLogOptions}.
123
+ * @returns {@link UseSecurityLogReturn}
124
+ *
125
+ * @remarks
126
+ * **ID counter** — the counter lives in a `useRef`.
127
+ * It is only ever incremented inside `pushEvent` and read to generate IDs —
128
+ * nothing renders based on its value.
129
+ *
130
+ * **Stable callbacks** — `pushEvent` and `makeClipboardHandler` are both
131
+ * wrapped in `useCallback`. `makeClipboardHandler` depends on `pushEvent`
132
+ * which is itself stable, so the returned handler references are stable across
133
+ * renders and safe to pass as props without triggering child re-renders.
134
+ *
135
+ * **Newest-first ordering** — events are prepended (`[newEvent, ...prev]`)
136
+ * so index 0 is always the most recent. This matches the expected rendering
137
+ * order for audit log UIs.
138
+ */
139
+ export declare const useSecurityLog: (options?: UseSecurityLogOptions) => UseSecurityLogReturn;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Public API barrel for the FieldShield library.
4
+ *
5
+ * Import from "fieldshield" to access all public components, hooks,
6
+ * utilities, types, and built-in patterns.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { FieldShieldInput, FIELDSHIELD_PATTERNS, collectSecureValues } from "fieldshield";
11
+ * ```
12
+ *
13
+ * @module fieldshield
14
+ */
15
+ export { FieldShieldInput } from "./components/FieldShieldInput";
16
+ export type { FieldShieldHandle, FieldShieldInputProps, SensitiveClipboardEvent, } from "./components/FieldShieldInput";
17
+ export { useFieldShield } from "./hooks/useFieldShield";
18
+ export type { CustomPattern, UseFieldShieldReturn } from "./hooks/useFieldShield";
19
+ export { useSecurityLog } from "./hooks/useSecurityLog";
20
+ export type { SecurityEvent, SecurityEventType, UseSecurityLogOptions, UseSecurityLogReturn, } from "./hooks/useSecurityLog";
21
+ export { collectSecureValues, purgeSecureValues } from "./utils/collectSecureValue";
22
+ export type { FieldShieldRefMap, SecureValues } from "./utils/collectSecureValue";
23
+ export { FIELDSHIELD_PATTERNS, OPT_IN_PATTERNS } from "./patterns";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Public API barrel for the FieldShield library.
4
+ *
5
+ * Import from "fieldshield" to access all public components, hooks,
6
+ * utilities, types, and built-in patterns.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { FieldShieldInput, FIELDSHIELD_PATTERNS, collectSecureValues } from "fieldshield";
11
+ * ```
12
+ *
13
+ * @module fieldshield
14
+ */
15
+ export { FieldShieldInput } from "./components/FieldShieldInput";
16
+ export type { FieldShieldHandle, FieldShieldInputProps, SensitiveClipboardEvent, } from "./components/FieldShieldInput";
17
+ export { useFieldShield } from "./hooks/useFieldShield";
18
+ export type { CustomPattern, UseFieldShieldReturn } from "./hooks/useFieldShield";
19
+ export { useSecurityLog } from "./hooks/useSecurityLog";
20
+ export type { SecurityEvent, SecurityEventType, UseSecurityLogOptions, UseSecurityLogReturn, } from "./hooks/useSecurityLog";
21
+ export { collectSecureValues, purgeSecureValues } from "./utils/collectSecureValue";
22
+ export type { FieldShieldRefMap, SecureValues } from "./utils/collectSecureValue";
23
+ export { FIELDSHIELD_PATTERNS, OPT_IN_PATTERNS } from "./patterns";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file patterns.ts
3
+ * @description Single source of truth for all built-in and opt-in sensitive data
4
+ * patterns shipped with FieldShield.
5
+ *
6
+ * @remarks
7
+ * Patterns are stored as plain regex source strings — no delimiters (`/`) and
8
+ * no flags. This is intentional for two reasons:
9
+ *
10
+ * 1. **Main thread consumers** (`FieldShieldInput.tsx`, `useFieldShield.ts`) import
11
+ * this file directly and construct `RegExp` objects with whatever flags they
12
+ * need for their specific use case.
13
+ *
14
+ * 2. **The Web Worker** (`fieldshield.worker.ts`) cannot safely import from a
15
+ * relative path when the library is consumed via npm — the bundler of the
16
+ * consuming application may not resolve worker imports correctly. Instead,
17
+ * `useFieldShield.ts` sends these patterns to the worker via a `CONFIG` message
18
+ * on mount, keeping the worker completely self-contained with no imports.
19
+ *
20
+ * **Updating patterns:**
21
+ * Change a pattern here and it automatically propagates to every consumer —
22
+ * the worker, the component paste handler, and the hook config message. There
23
+ * is no second place to update.
24
+ *
25
+ * **Adding a new pattern:**
26
+ * Add a new key/value pair to `FIELDSHIELD_PATTERNS`. The string must be a valid
27
+ * regex source. Use double backslashes for escape sequences since this is a
28
+ * string not a regex literal — `"\\d"` not `"\d"`.
29
+ *
30
+ * **Opt-in patterns:**
31
+ * Three patterns are exported separately in {@link OPT_IN_PATTERNS} because they
32
+ * produce unacceptably high false positive rates in free-text fields such as
33
+ * clinical notes. Use them via `customPatterns` only on fields where the specific
34
+ * data type is expected — see the JSDoc on each pattern for details.
35
+ *
36
+ * **Built-in pattern count: 13**
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { FIELDSHIELD_PATTERNS } from "../patterns";
41
+ *
42
+ * // Build a RegExp from a pattern string
43
+ * const regex = new RegExp(FIELDSHIELD_PATTERNS.SSN, "gi");
44
+ * regex.test("372-84-1950"); // true
45
+ * ```
46
+ *
47
+ * @module patterns
48
+ */
49
+ /**
50
+ * Built-in sensitive data patterns shipped with FieldShield (13 total).
51
+ *
52
+ * Keys are the pattern names surfaced in `findings` arrays and callback
53
+ * payloads (e.g. `"SSN"`, `"EMAIL"`). Values are regex source strings.
54
+ *
55
+ * Five additional patterns ({@link OPT_IN_PATTERNS}) are excluded from this
56
+ * set due to high false positive rates in free-text and clinical note fields.
57
+ * Use them via `customPatterns` only on fields where that data type is expected.
58
+ */
59
+ export declare const FIELDSHIELD_PATTERNS: Readonly<Record<string, string>>;
60
+ /**
61
+ * High false positive patterns — opt-in only.
62
+ *
63
+ * These five patterns were evaluated for inclusion in {@link FIELDSHIELD_PATTERNS}
64
+ * and excluded because they produce unacceptably high false positive rates in
65
+ * free-text fields, clinical notes, and general-purpose input fields.
66
+ *
67
+ * **Use via `customPatterns`** only on fields where the specific data type is
68
+ * known to be expected. Do not add these to free-text or clinical note fields.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * import { OPT_IN_PATTERNS } from "fieldshield";
73
+ *
74
+ * // Only on a payment or banking field
75
+ * <FieldShieldInput
76
+ * label="IBAN"
77
+ * customPatterns={[{ name: "IBAN", regex: OPT_IN_PATTERNS.IBAN }]}
78
+ * />
79
+ *
80
+ * // Only on a DEA number entry field (prescriber credentialing)
81
+ * <FieldShieldInput
82
+ * label="DEA Number"
83
+ * customPatterns={[{ name: "DEA_NUMBER", regex: OPT_IN_PATTERNS.DEA_NUMBER }]}
84
+ * />
85
+ *
86
+ * // Only on a dedicated wire transfer form field
87
+ * <FieldShieldInput
88
+ * label="Bank (SWIFT/BIC)"
89
+ * customPatterns={[{ name: "SWIFT_BIC", regex: OPT_IN_PATTERNS.SWIFT_BIC }]}
90
+ * />
91
+ *
92
+ * // Only on a provider lookup field
93
+ * <FieldShieldInput
94
+ * label="Provider NPI"
95
+ * customPatterns={[{ name: "NPI_NUMBER", regex: OPT_IN_PATTERNS.NPI_NUMBER }]}
96
+ * />
97
+ *
98
+ * // Only on a passport / identity verification field
99
+ * <FieldShieldInput
100
+ * label="Passport Number"
101
+ * customPatterns={[{ name: "PASSPORT_NUMBER", regex: OPT_IN_PATTERNS.PASSPORT_NUMBER }]}
102
+ * />
103
+ * ```
104
+ */
105
+ export declare const OPT_IN_PATTERNS: Readonly<Record<string, string>>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file collectSecureValues.ts
3
+ * @description Utility for securely retrieving values from multiple
4
+ * FieldShieldInput fields simultaneously on form submission.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * const refs = { ssn: ssnRef, apiKey: apiKeyRef, notes: notesRef };
9
+ *
10
+ * const handleSubmit = async () => {
11
+ * const values = await collectSecureValues(refs);
12
+ * await fetch("/api/submit", { body: JSON.stringify(values) });
13
+ *
14
+ * purgeSecureValues(refs);
15
+ * };
16
+ * ```
17
+ *
18
+ * @module collectSecureValues
19
+ */
20
+ import type { RefObject } from "react";
21
+ import type { FieldShieldHandle } from "../components/FieldShieldInput";
22
+ /**
23
+ * A map of field names to their FieldShieldHandle refs.
24
+ * Keys become the property names in the resolved values object.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const refs: FieldShieldRefMap = {
29
+ * ssn: ssnRef,
30
+ * apiKey: apiKeyRef,
31
+ * notes: notesRef,
32
+ * };
33
+ * ```
34
+ */
35
+ export type FieldShieldRefMap = Record<string, RefObject<FieldShieldHandle | null>>;
36
+ /**
37
+ * The resolved values object returned by {@link collectSecureValues}.
38
+ * Keys mirror the input `FieldShieldRefMap` — values are the retrieved
39
+ * plaintext strings, or `""` if the ref was unmounted or null.
40
+ */
41
+ export type SecureValues<T extends FieldShieldRefMap> = Record<keyof T, string>;
42
+ /**
43
+ * Retrieves real values from multiple FieldShieldInput fields in parallel
44
+ * via `Promise.allSettled`. Each value is fetched from the field's isolated Web
45
+ * Worker memory — no plaintext ever exists on the main thread until this
46
+ * call resolves.
47
+ *
48
+ * Null or unmounted refs resolve to `""` rather than throwing, so a missing
49
+ * optional field never blocks form submission.
50
+ *
51
+ * @param refs - Named map of FieldShieldHandle refs to collect from.
52
+ * @returns A promise resolving to an object with the same keys as `refs`,
53
+ * each containing the retrieved plaintext string.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * const ssnRef = useRef<FieldShieldHandle>(null);
58
+ * const notesRef = useRef<FieldShieldHandle>(null);
59
+ *
60
+ * const handleSubmit = async () => {
61
+ * const { ssn, notes } = await collectSecureValues({ ssn: ssnRef, notes: notesRef });
62
+ * await fetch("/api/patient", { body: JSON.stringify({ ssn, notes }) });
63
+ * };
64
+ * ```
65
+ *
66
+ * @remarks
67
+ * **Why named keys instead of an array?** An array of refs would resolve to
68
+ * an array of strings in positional order — callers would have to remember
69
+ * which index corresponds to which field. A named map produces a typed object
70
+ * where the field name is explicit at the call site and in the resolved value,
71
+ * making accidental field-order bugs impossible.
72
+ *
73
+ * **Why not a hook?** This function has no React state, no side effects, and
74
+ * no lifecycle dependency. Making it a hook would require consumers to call it
75
+ * inside `useCallback` and follow Rules of Hooks unnecessarily. A plain async
76
+ * function is the correct primitive here.
77
+ */
78
+ export declare const collectSecureValues: <T extends FieldShieldRefMap>(refs: T) => Promise<SecureValues<T>>;
79
+ /**
80
+ * Calls `purge()` on every ref in the map, zeroing out worker memory for all
81
+ * fields simultaneously. Call this immediately after `collectSecureValues`
82
+ * resolves and the data has been sent to your backend.
83
+ *
84
+ * Null or unmounted refs are silently skipped.
85
+ *
86
+ * @param refs - Named map of FieldShieldHandle refs to purge.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const values = await collectSecureValues(refs);
91
+ * await sendToBackend(values);
92
+ * purgeSecureValues(refs); // fire-and-forget, no await needed
93
+ * ```
94
+ *
95
+ * @remarks
96
+ * `purge()` is synchronous — it posts a PURGE message to the worker with no
97
+ * response awaited. Calling `purgeSecureValues` immediately after
98
+ * `collectSecureValues` is safe because both the real value retrieval and
99
+ * the purge message travel through the same worker message queue in order.
100
+ * The PURGE message will always be processed after the GET_TRUTH reply.
101
+ */
102
+ export declare const purgeSecureValues: (refs: FieldShieldRefMap) => void;