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,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;
|
package/dist/index.d.cts
ADDED
|
@@ -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";
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|