Releases: anuragnedunuri/fieldshield
FieldShield v1.1.4 — Cursor Drift Root Cause Fix
All v1.1.x releases have been CSS and UX refinements. The Web Worker isolation, DOM scrambling,
MessageChannel-based
GET_TRUTHdelivery, clipboard interception, and pattern detection architecture are unchanged since v1.0.0. Consumers who reviewed
FieldShield's threat model at v1.0 adoption do not need to re-review their security assumptions for any 1.1.x upgrade.
Fixed
Cursor drift root cause — identified and fixed
v1.1.1 forced monospace on .fieldshield-real-input but left .fieldshield-mask-layer inheriting the consumer's proportional font.
Because the real input contains scrambled xxxxx and the mask layer contains the actual typed characters with █ for sensitive spans, the two layers were rendering different strings. In proportional fonts different strings have different per-character advance widths, so the caret (positioned by the browser using monospace advances in the real input) landed over the wrong glyph in the mask layer starting from character 1. Pattern detection amplified the drift further because █ in most proportional fonts has a different advance than x in monospace.
Fix: apply the same monospace font stack (ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace !important) to .fieldshield-mask-layer and .fieldshield-grow in addition to .fieldshield-real-input. Both layers now render every glyph at identical pixel advance regardless of the consumer's page font.
Horizontal scroll sync
When typed content exceeded the field's visible width, the real input auto-scrolled its content to keep the caret visible but the mask layer (overflow: hidden, no native scroll) stayed put, leaving the caret on top of the wrong character. Added a scroll-sync effect that reads input.scrollLeft/scrollTop on every scroll event and writes an inverse transform: translate(...) to the mask layer. Scroll sync now
works for both single-line and textarea.
Vertical alignment (cursor above / below text)
Replaced display: flex; align-items: center on the mask layer with an
explicit line-height matched on both layers:
line-height: calc(var(--fieldshield-min-height) - 2 * var(--fieldshield-border-width));Native <input> and <div> compute line-height: normal inconsistently, so flex-centering produced a sub-pixel-to-multi-pixel offset between the caret and the mask text. Setting an explicit matching line-height on both layers makes them center identically.
Textarea line-wrap drift
Added box-sizing: border-box to .fieldshield-real-input. Previously content-box + width: 100% + padding gave the real input a content
area 24px wider than the mask layer, so textarea text wrapped at different column counts in each layer and the cursor drifted from the
second wrapped line onward.
Demo app mobile background
On narrow viewports (375px mobile) the dark terminal background stopped halfway down the page and showed the browser default grey below. Root cause: html, body, #root { height: 100% } fixed body at viewport height, so when stacked content exceeded 100vh the background ended at the body border. Fixed by changing to min-height: 100% so body grows with content, and adding background-color: var(--app-bg) to html as a belt-and-suspenders fallback. Demo-app-only change — library consumers unaffected.
Added
New CSS token --fieldshield-border-width (default 1px) The wrapper border width is now a design token. The line-height calculation on both layers derives from this token, so consumers who need to change the border width via the token get correct vertical centering automatically.
Tests
5 new Vitest unit tests (FieldShieldInput — mask layer font consistency) covering the overlay architecture invariants the font fix depends on. Total: 459 unit tests (was 454).
4 new Playwright E2E tests in a new e2e/cursor-alignment.spec.ts file: first-character cursor position, per-character tracking through structured SSN input, pattern-detection-does-not-move-cursor regression, and backspace-from-mid-string. Total: 49 E2E tests (was 45).
Known limitations
Design-system font integration
FieldShield fields currently render in monospace at all times. This is a structural consequence of the architecture: input.value is scrambled to xxxxx for security (so DOM scraping sees no real data), while the visible mask layer renders the actual typed characters with █ for sensitive spans. In proportional fonts, xxxxx and Hello have different total widths — the caret position (computed from the real input's character advances) diverges from where the mask layer paints the corresponding character. The only way to guarantee advance widths match across two different strings is to use a font where every character has the same advance width — monospace.
Consumer font support would require either (a) putting real characters in input.value (breaks the security model) or (b) per-character
runtime measurement (expensive and fragile). We are exploring options for this limitation in a future release.
Live demo
https://fieldshield-demo.vercel.app/
Architecture stability note
Upgrading within the 1.x line is safe for security review. Every release from v1.1.0 through v1.1.4 has been a CSS or UX refinement —
the Web Worker isolation, DOM scrambling, MessageChannel-based GET_TRUTH delivery, clipboard interception, paste pre-scan, and
pattern detection contracts are identical to v1.0.0.
Full Changelog: v1.1.3...v1.1.4
FieldShield v1.1.3 — CSS Inheritance Lockdown
What's changed
Fixed
-
CSS inheritance lockdown — six inheritable CSS properties now explicitly reset on
.fieldshield-mask-layerand.fieldshield-growso consumer parent styles cannot silently break cursor/text alignment:text-align— shifts visible text but not cursortext-indent— offsets first line away from cursor starttext-transform— changes glyph widths causing cursor driftfont-variant-ligatures— collapses 2 chars to 1 wider glyphfont-kerning— kern pairs shift advance widths cumulativelyhyphens— auto-breaks lines at different points than real input
-
text-alignandtext-indentalso reset on.fieldshield-real-inputso the cursor originates from the same edge as visible text. -
Textarea selector reliability — replaced
:has(textarea)CSS selector with[data-type="textarea"]attribute selector. More reliable across consumer app contexts where CSS import order or specificity affects:has(). -
Textarea minimum height — empty textarea now enforces a minimum 2-row height instead of collapsing to a single line.
Root cause
The standard Vite React template sets #root { text-align: center }. This cascades into FieldShield's mask layer, centering visible text while the cursor stays left-aligned. The demo app never exposed this — it sets no text-align on parent elements. This is a novel finding: CSS containment gaps exist beyond scoped selectors for overlay-based security components.
Tests
- 2 new Vitest unit tests (total: 454 passing)
- 7 new Playwright E2E tests (total: 45 passing)
Documentation
- README: new "CSS inheritance and customisation" section in Known Limitations
- THREAT_MODEL.md: Expanded Threat Landscape (April 2026) — AI screen-reading
assistants (Copilot Vision, Gemini Live) and browser extensions with DOM
access added as documented threat actors
Live demo
FieldShield v1.1.2 — Placeholder Fix & CSS Import Path
What's changed
Fixed
-
Placeholder blur — after the v1.1.1 monospace fix, the native
::placeholderwas rendering in monospace on top of the mask layer's
visible placeholder span, causing a ghost/double-text effect on empty fields. Fixed by setting.fieldshield-real-input::placeholder { color: transparent }.a11yModeis unaffected. -
CSS import path — added
"./style"to theexportsmap so consumerscan import the stylesheet with the short form:import "fieldshield/style"
FieldShield v1.1.1 — Cursor Drift in Proportional Fonts
What's changed
Fixed
- Cursor drift in proportional font environments —
.fieldshield-real-input
now enforces a monospace font stack with!important: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace
The real input is always color: transparent — consumers never see it, only the cursor. Without this fix, font-family: inherit caused the real
input to pick up the consumer's proportional font (Inter, Roboto, Arial), where character advance widths are not uniform. The cursor drifted
progressively further from the displayed text as more characters were typed.
- Root cause — the demo app used IBM Plex Mono which masked the bug during development. Consumer apps with proportional fonts exposed the drift.
Notes
- The mask layer (what users actually see) still inherits the consumer's font — visual output is unchanged.
.fieldshield-a11y-inputis unaffected — the password input retains
font-family: inheritso the browser's native dot masking renders correctly.- No consumer-side changes required.
Live demo
FieldShield v1.1.0 — Worker Blob URL, CSS Cursor Drift, CREDIT_CARD Pattern
What's changed
Fixed
-
Worker instantiation for npm consumers — replaced
new URL("../workers/fieldshield.worker.ts", import.meta.url)with a
blob URL via Vite's?worker&inlineimport. The previous approach referenced the TypeScript source file which does not exist in the
published npm package, causing a runtime worker failure for all npm consumers. The worker is now compiled and inlined intofieldshield.jsat build time — no separate worker file, no bundler configuration required. -
CSS cursor drift from letter/word spacing — added
letter-spacing: 0,word-spacing: 0, andfont-weight: inheritto.fieldshield-mask-layer,.fieldshield-real-input, and.fieldshield-grow. Without these, consumer stylesheets setting non-zero letter or word spacing on a parent element cascade unevenly into both overlay layers, causing the cursor to appear offset from the displayed masked text.
Changed
CREDIT_CARDpattern — broadened Mastercard prefix from5[1-5]to5\dto cover all IIN ranges; added6\d{3}variant for Discover and UnionPay cards. Luhn validation is still recommended post-match in production.
Documentation
- Updated Framework compatibility — the worker is now bundled inline; no per-bundler configuration (worker-loader, publicPath) is needed.
- Updated CSP section —
worker-src 'self' blob:is now required. Theblob:source is mandatory for the inlined worker to load.
Live demo
FieldShield v1.0.1 — Package metadata fix
[1.0.1] — 2026-04-07
Fixed
- Corrected repository URL, homepage, bugs, and author fields in
package.json— these contained placeholder values from initial scaffolding - Source maps excluded from npm package via
.npmignore— only compiled output ships to consumers - Worker output filename stabilized to
fieldshield.worker.js— previously included a content hash suffix that changed on every build
FieldShield v1.0.0 — Initial public release
Changelog
All notable changes to FieldShield are documented here.
This project follows Semantic Versioning:
- Patch (
1.0.x) — bug fixes, false positive/negative corrections to existing patterns - Minor (
1.x.0) — new patterns, new props, new features — backward compatible - Major (
x.0.0) — breaking API changes
Pattern updates are minor releases, not patches. A new pattern could start flagging content that was previously clean. Review pattern changes before upgrading.
[1.0.0] — 2026
Initial public release.
Architecture
- Web Worker isolation — real input value (
internalTruth) stored exclusively in a dedicated worker thread, never in the DOM - DOM scrambling —
input.valuealways contains scrambledxcharacters, never the real value - MessageChannel point-to-point delivery for
getSecureValue()— browser extensions monitoringpostMessagecannot intercept the response - Clipboard interception — copy and cut events write masked
█characters to the clipboard, not the real value - Paste interception — paste events are scanned before the browser inserts content;
onSensitivePastereturningfalseblocks the paste entirely - Worker initialization fallback — if the Worker constructor throws (e.g., strict CSP), the component automatically falls back to
a11yMode. - Worker message payload validation — UPDATE messages with invalid payload shapes are silently discarded
Props
label— visible label text linked viahtmlFor/id.type—"text"or"textarea"with auto-grow supportplaceholder— forwarded to native elementa11yMode— renderstype="password"for WCAG 2.1 AA / Section 508 compliance; auto-activated on worker init failurecustomPatterns— additional patterns layered on top of built-in defaults; use withOPT_IN_PATTERNSfor field-specific opt-in patternsmaxProcessLength— blocks input exceeding the character limit (default100_000); blocking rather than truncating prevents blind spotsonMaxLengthExceeded— called when input is blocked bymaxProcessLength.onSensitiveCopyAttempt— fired on copy/cut when sensitive patterns are presentonSensitivePaste— fired on paste when sensitive patterns are detected; returnfalseto block the pasteonWorkerError— fired when the worker encounters a runtime erroronChange— fires after each worker UPDATE with masked value and findingsdisabled,required,maxLength,rows,inputMode,className,style,onFocus,onBlur.
Ref methods
getSecureValue()— retrieves real value from worker memory via private MessageChannel; rejects after 3-second timeoutpurge()— zerosinternalTruthin worker memory
Hooks and utilities
useFieldShield— hook managing worker lifecycle, pattern detection, and secure value retrievaluseSecurityLog— capped, auto-timestamped audit event log withmakeClipboardHandler,pushEvent,clearLog.collectSecureValues— parallelgetSecureValue()across multiple fields viaPromise.allSettled.purgeSecureValues— simultaneouspurge()across multiple fields
Built-in patterns
13 active by default — enabled on every FieldShieldInput without configuration.
PII (6): SSN, EMAIL, PHONE, CREDIT_CARD, DATE_OF_BIRTH, TAX_ID.
Healthcare (1): UK_NIN.
Credentials (6): AI_API_KEY, AWS_ACCESS_KEY, GITHUB_TOKEN, STRIPE_KEY, JWT, PRIVATE_KEY_BLOCK
Opt-in (5): IBAN, DEA_NUMBER, SWIFT_BIC, NPI_NUMBER, PASSPORT_NUMBER — exported via OPT_IN_PATTERNS, not active by default. These patterns produce unacceptably high false positive rates in free-text and clinical note fields. Use via customPatterns only on fields where the specific data type is expected.
import { OPT_IN_PATTERNS } from "fieldshield";
<FieldShieldInput
label="DEA Number."
customPatterns={[{ name: "DEA_NUMBER", regex: OPT_IN_PATTERNS.DEA_NUMBER }]}
/>Security
- No-network guarantee — worker contains zero
fetch(),XMLHttpRequest,WebSocket,EventSource, orsendBeacon()calls - CSP guidance —
worker-src 'self' blob:recommended for regulated deployments THREAT_MODEL.md— full threat model with 9 mitigated threats, 9 unmitigated threats, environment assumptions, residual risk table, and compliance mapping
Documentation
README.md— full API documentation, framework compatibility (Vite, Webpack 4/5, Next.js, SSR), form library integration (RHF, Formik, Zod), CSP guidance, known limitations, compliance notesTHREAT_MODEL.md— threat model for security engineers and compliance auditorsLICENSE— MIT
Test coverage
- Vitest unit tests — 454 tests across 7 modules
- Playwright e2e tests — 38 tests covering real clipboard, worker isolation, DOM protection, worker fallback, accessibility
Known limitations
realValueRefexists on the main thread while the user is actively typing — readable by debuggers and privileged extensions- No
idprop override —useId()generates stable IDs automatically nameprop not supported — native form submission not supported; usegetSecureValue()on submitonCopy/onCutprops not forwarded — useonSensitiveCopyAttemptinstead- IME composition (CJK input) not supported — use
a11yModeas fallback - No cross-field PHI combination detection — planned for v2.0
- Names and addresses cannot be detected with regex — server-side NER required