Skip to content

Comprehensive review fixes: security, a11y, perf, tests#5

Merged
kranthie merged 3 commits into
mainfrom
review/comprehensive-fixes
Apr 18, 2026
Merged

Comprehensive review fixes: security, a11y, perf, tests#5
kranthie merged 3 commits into
mainfrom
review/comprehensive-fixes

Conversation

@kranthie
Copy link
Copy Markdown
Owner

Summary

Synthesis of a five-agent review (security, performance, code quality, testing, a11y) run against main before this branch. Changes are grouped in one PR because almost every area was small-per-finding; the unified story is easier to review than 8 mini-PRs.

Everything passes pnpm turbo lint typecheck test build. Build still emits 784 static pages.

What's in

Security

  • Vulnerable dep bumps — DOMPurify 3.3.33.4.0, isomorphic-dompurify → 3.9.0, js-yaml → 4.1.1. Resolves the mutation-XSS + ADD_TAGS/FORBID_TAGS bypass advisories and js-yaml merge-key prototype-pollution.
  • Replace regex SVG sanitizer in svg/svg-viewer with DOMPurify's SVG profile. The hand-rolled regex was bypassable via HTML-entities, CDATA, and url(javascript:…) in <style>.
  • Tighten iframe sandbox — new htmlPreviewAllowScripts on ToolUIConfig is opt-in. html/playground opts in; the other ~37 static-HTML-renderer tools (SVG generators, QR codes, meta previews, etc.) now render without allow-scripts/allow-forms — sanitizer bypasses can no longer escalate to exec/form-post.
  • PBKDF2 600k in crypto/aes-encrypt + crypto/aes-decrypt (OWASP current). Ciphertext from the old 100k setting will no longer decrypt; documented trade-off acceptable given nothing persists it server-side.
  • URL-safe base64 in jwt/fernet-encoder (Fernet spec requires it for round-tripping with Python's cryptography.fernet).
  • CSP — drop cdn.jsdelivr.net (unused anywhere), add base-uri 'self' and form-action 'self' https://api.staticforms.dev. Kept 'unsafe-inline'/'unsafe-eval' — Mermaid and hydration still need them; follow-up work.
  • JWT Decoder description now warns that signatures are NOT verified.

Accessibility

  • aria-haspopup="dialog" on hero and header search buttons.
  • Removed onCloseAutoFocus={preventDefault} on theme + locale dropdowns so Radix returns focus to the trigger on Esc.
  • aria-hidden on decorative canvas (code-rain) and typewriter cursor |.
  • useTypewriter now respects prefers-reduced-motion (JS timers bypassed the CSS rule).
  • Light-mode --muted-foreground #737373#616161 (AA on --muted). Removed the high-contrast media-query opacity: 0.8 override that was making it worse.
  • File-upload control is now a real <button> that triggers a hidden input — keyboard users no longer get a dead focus stop on an sr-only input.
  • Contact form previously ignored missing fields silently. Now: per-field error rendering, role="alert" live region on a summary message, email format validation, errors clear on input. New i18n keys added.

Performance

  • LazyMotion + m.* — new MotionProvider wraps the homepage marketing area. All five marketing components migrated motion.*m.*. Drops ~60-80 KB gz on the homepage.
  • Dynamic-import qrcode.react in demo-visuals so it isn't pulled into the home entry chunk until the demo cycles in.
  • Dynamic-import MarkdownPreview in tool-documentation (below-the-fold; pulls rehype-highlight + full highlight.js).
  • tool-layout: read localStorage in the state initializer with a typeof window guard instead of via useEffect — no cascading re-render, no React Compiler warning.
  • downloadFilename memoized per result across standard/diff/generator layouts — previously re-built on every render.

Code quality / bugs

  • Fix scaffold generator (packages/tools/scripts/generate-tool.ts): generated tests were missing await; every new tool scaffold started with failing tests.
  • Replace hardcoded "Example loaded" toast + "Enter X input..." placeholder with i18n keys (drift noted by the review because only en is active today — this matters the moment non-English locales return).
  • Executor — 5 MB size guard now runs before Zod validation (Zod was allocating/parsing pathological inputs before rejection). getByteSize returns Infinity on unserializable inputs (circular refs) so the guard rejects them.
  • Secure randommath/random-number-generator and color/random-color now use crypto.getRandomValues + rejection sampling. Math.round(Math.random() * range) had well-known boundary bias that users of a "Random Number Generator" don't expect.
  • useClipboard + DownloadButton track their reset timers in refs and clear on unmount / reinvocation — prevents overlapping timers and setState-after-unmount warnings.

Dead code removal

  • Delete useWorker / useWorkerToolExecution hooks + public/workers/tool-worker.js — never imported. Ships no code but kept Comlink in the graph and the worker asset in out/.
  • Delete apps/web/components/index.ts (unused barrel).
  • Delete apps/web/lib/tools/schema-to-json.ts (dead re-export).

Testing additions

  • Executor — explicit EXEC_TIMEOUT test (fake timers) and EXEC_FAILED "Input too large" test, including circular refs.
  • Registry — tests for invalid tool-id format, unknown category in id, bad category id to getByCategory.
  • register.test.ts — every registered tool's id prefix matches meta.category and a known CATEGORIES entry; IDs are globally unique; no CATEGORIES entry is orphaned (empty).
  • i18n parity — new test asserts every non-English messages/*.json has the same key set as en.json. Patched the 10 other locale files with English fallbacks so the test starts green; pnpm translate can replace them.
  • Page budget — new test asserts locales × (tools + categories + static) stays below Cloudflare Pages' 19,500-page ceiling. Guards against re-enabling locales without re-checking.

What's not in (deliberate follow-ups)

  • @utils-live/tools barrel side-effect import (register.ts) still forces the full registry into every tool page's chunk (~770 KB gz). Review called this the top perf win. It's an architectural change (per-tool subpath exports) that deserves its own PR.
  • Replacing CSP 'unsafe-inline' / 'unsafe-eval' with hashes/nonces — needs a Pages Function and more thorough audit of Mermaid / Chart.js runtime behavior.
  • Bump next-intl 3.26.5 → 4.9.1 (open-redirect advisory). The vulnerable redirect helper is not used in this codebase per the security review, so the advisory isn't reachable. Major bump was skipped to keep this PR focused.
  • Making pnpm turbo test actually run coverage (testing review found the "100% coverage" claim isn't enforced — current real coverage on the tools package is ~82% lines / 65% branches). Needs honest threshold selection; not in scope here.

Test plan

  • pnpm install (deps resolved)
  • pnpm turbo lint (0 errors; 4 pre-existing tanstack-compatibility warnings)
  • pnpm turbo typecheck (clean)
  • pnpm turbo test — 6,636 + 123 tests pass
  • pnpm turbo build — 784 static pages generated
  • Smoke-test the homepage hero (search button, category links)
  • Smoke-test a tool page that uses outputRenderer: "html" (svg-viewer, html-playground) — verify preview still renders
  • Smoke-test AES encrypt → AES decrypt round-trip with the new 600k iterations
  • Smoke-test Contact form submission with empty fields (should show validation) and with invalid email
  • Verify dark-mode focus rings are still visible on buttons

🤖 Generated with Claude Code

…ests

Synthesis of a multi-agent review covering security, a11y, perf, code
quality, and testing. Grouped by theme:

Security
- Bump DOMPurify 3.3.3 -> 3.4.0, isomorphic-dompurify -> 3.9.0, js-yaml
  -> 4.1.1 to pick up published XSS / prototype-pollution fixes.
- Replace hand-rolled regex sanitizer in svg/svg-viewer with DOMPurify
  (SVG profile, explicit FORBID_TAGS/FORBID_ATTR).
- Gate <HtmlPreview> script/form sandbox permissions behind opt-in
  htmlPreviewAllowScripts (ToolUIConfig); only HTML playground opts in.
- Raise AES-GCM PBKDF2 iterations 100k -> 600k (encrypt+decrypt in
  lockstep) to match current OWASP guidance.
- Emit URL-safe base64 from jwt/fernet-encoder per Fernet spec.
- Warn in jwt/jwt-decoder description that signatures are NOT verified.
- Tighten CSP in public/_headers: drop unused cdn.jsdelivr.net, add
  base-uri and form-action self-only.

Accessibility
- aria-haspopup="dialog" on hero + header search buttons.
- Drop onCloseAutoFocus={preventDefault} from theme / locale dropdown
  menus so Radix returns focus to the trigger.
- aria-hidden decorative canvas (code-rain) and typewriter cursor.
- Respect prefers-reduced-motion in tool-demo typewriter (JS-driven).
- Darken light-mode --muted-foreground (#737373 -> #616161) to hit AA
  on --muted; remove opacity:0.8 override that worsened it.
- Rewire file-upload to a real <button> that triggers a hidden input,
  with proper aria-label and tabIndex.
- Contact form now validates on submit with per-field errors, live-
  region alert, and email-format check. i18n keys added.

Performance
- LazyMotion(domAnimation) wrapper via MotionProvider; migrate all
  marketing components from motion.* -> m.* to drop eager framer-motion
  feature set (~60-80 KB gz).
- Dynamic-import qrcode.react in demo-visuals so the homepage entry
  chunk doesn't ship it.
- Dynamic-import MarkdownPreview in tool-documentation (below-the-fold;
  pulls rehype-highlight + highlight.js).
- tool-layout now reads localStorage in the state initializer with a
  typeof-window guard instead of via useEffect, avoiding a cascading
  re-render and the React Compiler warning.
- Memoize downloadFilename per result across all three tool layouts.

Code quality / bugs
- Fix scaffold generator (scripts/generate-tool.ts): generated tests
  were missing await; any new tool scaffold failed on first run.
- Replace hardcoded "Example loaded" toast + "Enter X input..."
  placeholder with i18n keys (t("exampleLoaded") / t("inputPlaceholder")).
- Reorder executor.ts: 5MB size guard runs before Zod validation so a
  huge input can't waste a full parse; getByteSize returns Infinity on
  unserializable inputs (circular refs) so the guard rejects them.
- math/random-number-generator + color/random-color now use
  crypto.getRandomValues with rejection sampling; Math.round() of
  Math.random() had boundary bias.
- use-clipboard and shared/download-button track their reset timers in
  refs and clear on unmount to prevent leaked setState-after-unmount.

Dead code
- Delete unused useWorker / useWorkerToolExecution hooks and
  public/workers/tool-worker.js (never imported anywhere; keeping the
  worker file meant comlink stayed in the client graph for nothing).
  Removed the stale worker-code-execution security test that grep'd
  the deleted file.
- Delete unused apps/web/components/index.ts barrel.
- Delete unused apps/web/lib/tools/schema-to-json.ts (re-export of a
  converter nothing imports).

Testing
- Executor: add coverage for the timeout path (fake timers) and the
  5MB input-size guard (including circular refs).
- Registry: add guards for invalid tool-id format, unknown category,
  and bad category ID passed to getByCategory.
- register.test: assert every registered tool's id prefix matches its
  meta.category and a known CATEGORIES entry; assert IDs are unique;
  assert no CATEGORY is orphaned.
- New i18n-parity test: every non-English messages/*.json must have
  the same key set as en.json. Patched the 10 locale files with
  English fallbacks so the test starts green.
- New page-budget test: locales x (tools + categories + static) stays
  under Cloudflare Pages' 19,500-page ceiling.
- Update three tests that assumed the old behavior (non-URL-safe base64
  in fernet, getByteSize(circular) === 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 18, 2026

Deploying utils-live with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8bd6952
Status: ✅  Deploy successful!
Preview URL: https://7171f327.utils-live.pages.dev
Branch Preview URL: https://review-comprehensive-fixes.utils-live.pages.dev

View logs

Kranthi Kumar Muppala and others added 2 commits April 18, 2026 12:41
Monaco ships via a CDN-hosted AMD loader: @monaco-editor/react fetches
https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js at
runtime. Dropping jsdelivr from script-src broke every tool page.

Restore jsdelivr in script-src, script-src-elem, style-src (Monaco
theme CSS), font-src (Monaco fonts), and connect-src (chunked vs/*
fetches). Also pin script-src-elem explicitly so the Monaco <script>
element is checked against the right directive (browsers only fall
back to script-src when script-src-elem is absent, which made the
failure mode harder to read in the console).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sortability test asserted that two KSUIDs generated back-to-back
share the same 5-char second-resolution prefix. On slow CI runners the
two generations can straddle a 1-second boundary, causing the prefixes
to differ (the test comment already acknowledged this would happen
"differ by at most 1 second" but then asserted strict equality).

Assert the property that actually matters for KSUIDs — monotonic sort
order — instead of prefix equality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kranthie kranthie merged commit 8eb1469 into main Apr 18, 2026
6 checks passed
@kranthie kranthie deleted the review/comprehensive-fixes branch April 18, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant