Comprehensive review fixes: security, a11y, perf, tests#5
Merged
Conversation
…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>
Deploying utils-live with
|
| 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 |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Synthesis of a five-agent review (security, performance, code quality, testing, a11y) run against
mainbefore 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
3.3.3→3.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.svg/svg-viewerwith DOMPurify's SVG profile. The hand-rolled regex was bypassable via HTML-entities, CDATA, and url(javascript:…) in<style>.htmlPreviewAllowScriptsonToolUIConfigis opt-in.html/playgroundopts in; the other ~37 static-HTML-renderer tools (SVG generators, QR codes, meta previews, etc.) now render withoutallow-scripts/allow-forms— sanitizer bypasses can no longer escalate to exec/form-post.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.jwt/fernet-encoder(Fernet spec requires it for round-tripping with Python'scryptography.fernet).cdn.jsdelivr.net(unused anywhere), addbase-uri 'self'andform-action 'self' https://api.staticforms.dev. Kept'unsafe-inline'/'unsafe-eval'— Mermaid and hydration still need them; follow-up work.Accessibility
aria-haspopup="dialog"on hero and header search buttons.onCloseAutoFocus={preventDefault}on theme + locale dropdowns so Radix returns focus to the trigger on Esc.aria-hiddenon decorative canvas (code-rain) and typewriter cursor|.useTypewriternow respectsprefers-reduced-motion(JS timers bypassed the CSS rule).--muted-foreground#737373→#616161(AA on--muted). Removed the high-contrast media-queryopacity: 0.8override that was making it worse.<button>that triggers a hidden input — keyboard users no longer get a dead focus stop on ansr-onlyinput.errorrendering,role="alert"live region on a summary message, email format validation, errors clear on input. New i18n keys added.Performance
m.*— newMotionProviderwraps the homepage marketing area. All five marketing components migratedmotion.*→m.*. Drops ~60-80 KB gz on the homepage.qrcode.reactindemo-visualsso it isn't pulled into the home entry chunk until the demo cycles in.MarkdownPreviewintool-documentation(below-the-fold; pulls rehype-highlight + full highlight.js).tool-layout: read localStorage in the state initializer with atypeof windowguard instead of viauseEffect— no cascading re-render, no React Compiler warning.downloadFilenamememoized per result across standard/diff/generator layouts — previously re-built on every render.Code quality / bugs
packages/tools/scripts/generate-tool.ts): generated tests were missingawait; every new tool scaffold started with failing tests."Example loaded"toast +"Enter X input..."placeholder with i18n keys (drift noted by the review because onlyenis active today — this matters the moment non-English locales return).getByteSizereturnsInfinityon unserializable inputs (circular refs) so the guard rejects them.math/random-number-generatorandcolor/random-colornow usecrypto.getRandomValues+ rejection sampling.Math.round(Math.random() * range)had well-known boundary bias that users of a "Random Number Generator" don't expect.useClipboard+DownloadButtontrack their reset timers in refs and clear on unmount / reinvocation — prevents overlapping timers and setState-after-unmount warnings.Dead code removal
useWorker/useWorkerToolExecutionhooks +public/workers/tool-worker.js— never imported. Ships no code but kept Comlink in the graph and the worker asset inout/.apps/web/components/index.ts(unused barrel).apps/web/lib/tools/schema-to-json.ts(dead re-export).Testing additions
EXEC_TIMEOUTtest (fake timers) andEXEC_FAILED "Input too large"test, including circular refs.getByCategory.register.test.ts— every registered tool's id prefix matchesmeta.categoryand a knownCATEGORIESentry; IDs are globally unique; noCATEGORIESentry is orphaned (empty).messages/*.jsonhas the same key set asen.json. Patched the 10 other locale files with English fallbacks so the test starts green;pnpm translatecan replace them.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/toolsbarrel 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.'unsafe-inline'/'unsafe-eval'with hashes/nonces — needs a Pages Function and more thorough audit of Mermaid / Chart.js runtime behavior.next-intl3.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.pnpm turbo testactually 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 passpnpm turbo build— 784 static pages generatedoutputRenderer: "html"(svg-viewer, html-playground) — verify preview still renders🤖 Generated with Claude Code