From 584271a195250b7bd2950943012568ce15e0bdf9 Mon Sep 17 00:00:00 2001 From: William Zujkowski Date: Tue, 6 Oct 2026 09:33:06 -0400 Subject: [PATCH 1/2] fix(web): resolve 2xl layout, section sorting, security hardening, and bundle footprint - Fix 2xl viewport layout distortion in BaseLayout.astro by anchoring footer as a full-width bottom child - Resolve section sorting bug (parseFloat evaluating Title 42 Section 2000e-1 in scientific notation) with compareSectionNumbers - Harden CSP with base-uri, object-src, and form-action directives - Replace chapter full-text innerHTML DOM assignment with document.adoptNode - Add --color-amber dark mode token (#FFBE2E) for WCAG AA contrast on navy - Decouple sanitizeExcerpt into sanitize.ts to prevent bundling Octokit on every page - Add OptionalHttpUrlSchema to avoid crashing on empty or unpinned CourtListener source URLs - Cap MAX_DECOMPRESSED_BYTES to 500 MiB within V8 MAX_STRING_LENGTH - Add randomized jitter to retry exponential backoff - Exclude src/__tests__ from tsconfig.build.json across packages to speed up Vitest --- apps/web/package.json | 1 + .../src/__tests__/markdown-sanitize.test.ts | 11 ++++ apps/web/src/components/ReadingToolbar.svelte | 43 ++++++++---- apps/web/src/components/SearchBar.svelte | 2 +- apps/web/src/components/SummaryBox.astro | 40 ++++++++---- apps/web/src/layouts/BaseLayout.astro | 14 ++-- apps/web/src/lib/github.ts | 65 +++---------------- apps/web/src/lib/sanitize.ts | 26 ++++++++ apps/web/src/pages/browse/[title].astro | 25 ++++--- .../src/pages/browse/[title]/[chapter].astro | 14 ++-- apps/web/src/pages/statute/[...slug].astro | 14 ++-- apps/web/src/styles/global.css | 16 +---- package.json | 2 - packages/annotator/src/annotator.ts | 11 +++- packages/annotator/src/client.ts | 3 +- packages/annotator/tsconfig.build.json | 3 +- packages/fetcher/src/constants.ts | 8 +-- packages/fetcher/tsconfig.build.json | 3 +- packages/shared/src/__tests__/sort.test.ts | 39 +++++++++++ packages/shared/src/index.ts | 1 + packages/shared/src/retry.ts | 6 +- packages/shared/src/sort.ts | 15 +++++ packages/types/src/__tests__/schemas.test.ts | 5 ++ packages/types/src/index.ts | 11 +++- packages/types/tsconfig.build.json | 3 +- pnpm-lock.yaml | 9 +-- 26 files changed, 239 insertions(+), 151 deletions(-) create mode 100644 apps/web/src/lib/sanitize.ts create mode 100644 packages/shared/src/__tests__/sort.test.ts create mode 100644 packages/shared/src/sort.ts diff --git a/apps/web/package.json b/apps/web/package.json index c5f2c8b..3e7a72f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@astrojs/markdown-remark": "^7.3.1", "@astrojs/rss": "^4.0.19", "@astrojs/svelte": "^9.0.1", + "@civic-source/shared": "workspace:*", "@civic-source/types": "workspace:*", "@fontsource/public-sans": "^5.3.0", "@octokit/rest": "^22.0.1", diff --git a/apps/web/src/__tests__/markdown-sanitize.test.ts b/apps/web/src/__tests__/markdown-sanitize.test.ts index 06e0c9d..c4f775b 100644 --- a/apps/web/src/__tests__/markdown-sanitize.test.ts +++ b/apps/web/src/__tests__/markdown-sanitize.test.ts @@ -14,7 +14,18 @@ describe('markdown sanitization wiring', () => { it('astro config registers rehype-sanitize as a markdown rehype plugin', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const unifiedOptions = (astroConfig.markdown as any)?.unified?.options; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const plugins = unifiedOptions?.rehypePlugins ?? (astroConfig.markdown as any)?.rehypePlugins ?? []; expect(plugins).toContain(rehypeSanitize); }); + + it('unified markdown renderer strips malicious script tags from output', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const unifiedProcessor = (astroConfig.markdown as any)?.unified; + expect(unifiedProcessor).toBeDefined(); + const renderer = await unifiedProcessor.createRenderer({}); + const result = await renderer.render('# Section Title\n\n\n\nLegitimate statutory text.'); + expect(result.code).not.toContain(' @@ -91,7 +110,7 @@ class="inline-flex items-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 py-1 text-xs font-medium text-slate shadow-2xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700" aria-label="Toggle text size" > - Size: + Size: {textSize === 'sm' ? '17px' : textSize === 'base' ? '19px' : '21px'} @@ -102,7 +121,7 @@ class="inline-flex items-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 py-1 text-xs font-medium text-slate shadow-2xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700" aria-label="Toggle reading line width" > - Width: + Width: {measureMode === 'standard' ? 'Standard' : 'Wide'} diff --git a/apps/web/src/components/SearchBar.svelte b/apps/web/src/components/SearchBar.svelte index 405aea5..17c3102 100644 --- a/apps/web/src/components/SearchBar.svelte +++ b/apps/web/src/components/SearchBar.svelte @@ -1,5 +1,5 @@ diff --git a/apps/web/src/layouts/BaseLayout.astro b/apps/web/src/layouts/BaseLayout.astro index 8cdad07..3bf386f 100644 --- a/apps/web/src/layouts/BaseLayout.astro +++ b/apps/web/src/layouts/BaseLayout.astro @@ -54,7 +54,7 @@ const titleEntries = Object.entries(TITLE_NAMES) - + {title === 'US Code Tracker' ? title : `${title} | US Code Tracker`}