From de9c35b3240cc2176908a30708d8fe4d554d7ada Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Mon, 5 Oct 2026 03:46:21 +0530 Subject: [PATCH 1/2] fix(reader): remove unsupported Review Pack banner --- src/components/HomeClient.tsx | 9 ---- src/components/ReviewPackBanner.tsx | 77 ----------------------------- 2 files changed, 86 deletions(-) delete mode 100644 src/components/ReviewPackBanner.tsx diff --git a/src/components/HomeClient.tsx b/src/components/HomeClient.tsx index 50fe48e..321c6dc 100644 --- a/src/components/HomeClient.tsx +++ b/src/components/HomeClient.tsx @@ -30,9 +30,6 @@ import { useAuth } from './AuthProvider'; import { LibraryEmptyOnboarding } from './LibraryEmptyOnboarding'; const Navbar = lazy(() => import('./Navbar').then((m) => ({ default: m.Navbar }))); -const ReviewPackBanner = lazy(() => - import('./ReviewPackBanner').then((m) => ({ default: m.ReviewPackBanner })) -); import { Badge } from './ui/badge'; import { Button } from './ui/button'; import { SegmentedControl } from './ui/segmented-control'; @@ -872,12 +869,6 @@ export default function HomeClient() { - {articles.length > 0 && ( - - - - )} - {articles.length > 0 && (
{ - if (typeof window === 'undefined') return false; - return localStorage.getItem(DISMISSED_KEY) === '1'; - }); - - if (dismissed) return null; - - const dismiss = () => { - localStorage.setItem(DISMISSED_KEY, '1'); - setDismissed(true); - }; - - return ( -
- - -
-
-
- - - Review Pack · Coming soon - -
-

- Turn your highlights into memory — automatically. -

-

- Reader builds a study pack from everything you've annotated. Read once. Remember - forever. -

- -
    - {perks.map(({ icon: Icon, label }) => ( -
  • - - {label} -
  • - ))} -
-
- -
- - Free while in preview -
-
-
- ); -} From 243a77b20c542ddf95408ef0c259bc31f3d68c2e Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Mon, 5 Oct 2026 04:13:56 +0530 Subject: [PATCH 2/2] fix(reader): patch Astro cache semantics advisory --- package.json | 9 ++- patches/README.md | 20 ++++++ patches/http-cache-semantics@4.3.0.patch | 15 ++++ pnpm-lock.yaml | 14 ++-- scripts/check-docs.mjs | 3 +- scripts/http-cache-security.test.mjs | 91 ++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 patches/README.md create mode 100644 patches/http-cache-semantics@4.3.0.patch create mode 100644 scripts/http-cache-security.test.mjs diff --git a/package.json b/package.json index 87fb661..d25a217 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,10 @@ "quality:duplication": "node scripts/check-code-health.mjs duplication", "quality:cycles": "node scripts/check-code-health.mjs cycles", "quality:dependencies": "node scripts/check-code-health.mjs dependencies", + "test:dependency-security": "node --test scripts/http-cache-security.test.mjs", "quality:suppressions": "node scripts/check-code-health.mjs suppressions", "quality:hygiene": "node scripts/check-code-health.mjs hygiene", - "quality": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm --filter web-annotator-extension type-check && pnpm --filter web-annotator-extension test && pnpm quality:unused && pnpm quality:complexity && pnpm quality:duplication && pnpm quality:cycles && pnpm quality:dependencies && pnpm quality:suppressions && pnpm docs:check && pnpm cf:build && pnpm --filter web-annotator-extension build && pnpm quality:hygiene", + "quality": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm --filter web-annotator-extension type-check && pnpm --filter web-annotator-extension test && pnpm quality:unused && pnpm quality:complexity && pnpm quality:duplication && pnpm quality:cycles && pnpm test:dependency-security && pnpm quality:dependencies && pnpm quality:suppressions && pnpm docs:check && pnpm cf:build && pnpm --filter web-annotator-extension build && pnpm quality:hygiene", "test:guest-pdf": "node --test scripts/qualify-guest-pdf.mjs scripts/qualify-import-signin.mjs", "test:account-pdf": "node --test scripts/qualify-account-pdf.mjs", "test:account-notes": "vitest run --config vitest.account-notes.config.ts" @@ -137,7 +138,11 @@ "brace-expansion@>=2.0.0 <2.1.7": "2.1.7", "brace-expansion@>=5.0.0 <5.0.12": "5.0.12", "undici@>=7.0.0 <7.29.1": "7.29.1", - "devalue@<=5.9.2": "5.9.3" + "devalue@<=5.9.2": "5.9.3", + "http-cache-semantics": "4.3.0" + }, + "patchedDependencies": { + "http-cache-semantics@4.3.0": "patches/http-cache-semantics@4.3.0.patch" } } } diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..199aa3d --- /dev/null +++ b/patches/README.md @@ -0,0 +1,20 @@ +# HTTP cache semantics security patch + +GitHub's reviewed advisory for GHSA-ch52-4w7c-c8xp currently has no upstream +patched version. The published `http-cache-semantics@4.3.0` source also needs a +behavioral fix: client `max-stale` must not bypass non-storable or revalidation +rules, or shared-cache restrictions for `proxy-revalidate` and `Set-Cookie`. + +This workspace pins the existing Astro build dependency to 4.3.0 and applies a +small local patch to those checks. The Reader landing's only affected path is +`landing-astro > astro > http-cache-semantics`; this does not add a runtime +dependency or a vulnerability allowlist. + +The lockfile integrity, patch hash, and patched snapshot were synchronized +from the reviewed PostTrainLLM #191 resolution and checked by a subsequent +Fleet Workspace frozen install; no unrelated lock nodes were changed. + +`pnpm test:dependency-security` exercises 27 cache behavior cases against the +package resolved through the `landing-astro` Astro install, including policy +serialization and ordinary `max-stale` age and URL bounds. Keep the patch until +the upstream source itself passes the same behavior tests. diff --git a/patches/http-cache-semantics@4.3.0.patch b/patches/http-cache-semantics@4.3.0.patch new file mode 100644 index 0000000..5b83088 --- /dev/null +++ b/patches/http-cache-semantics@4.3.0.patch @@ -0,0 +1,15 @@ +diff --git a/index.js b/index.js +--- a/index.js ++++ b/index.js +@@ -399,1 +399,10 @@ +- if (this._rescc['must-revalidate']) { ++ if ( ++ this._rescc['must-revalidate'] || ++ !this.storable() || ++ this._rescc['no-cache'] || ++ (this._isShared && ( ++ this._rescc['proxy-revalidate'] || ++ (this._resHeaders['set-cookie'] && ++ !this._rescc.public && !this._rescc.immutable) ++ )) ++ ) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4aa3b0..dc17e16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ overrides: brace-expansion@>=5.0.0 <5.0.12: 5.0.12 undici@>=7.0.0 <7.29.1: 7.29.1 devalue@<=5.9.2: 5.9.3 + http-cache-semantics: 4.3.0 + +patchedDependencies: + http-cache-semantics@4.3.0: + hash: 1bde1fe699a4c0f618ade5961734d1d414333292a27feacc6780a501009c0c74 + path: patches/http-cache-semantics@4.3.0.patch importers: @@ -3742,8 +3748,8 @@ packages: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} engines: {node: '>=20.19.0'} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} @@ -7472,7 +7478,7 @@ snapshots: get-tsconfig: 5.0.0-beta.4 github-slugger: 2.0.0 html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 + http-cache-semantics: 4.3.0(patch_hash=1bde1fe699a4c0f618ade5961734d1d414333292a27feacc6780a501009c0c74) js-yaml: 4.3.2 jsonc-parser: 3.3.1 magic-string: 1.4.2 @@ -8199,7 +8205,7 @@ snapshots: domutils: 4.0.2 entities: 8.0.0 - http-cache-semantics@4.2.0: {} + http-cache-semantics@4.3.0(patch_hash=1bde1fe699a4c0f618ade5961734d1d414333292a27feacc6780a501009c0c74): {} husky@9.1.7: {} diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 564e9a8..3597fa8 100755 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -20,8 +20,9 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs'; import { dirname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; -const ROOT = resolve(new URL('..', import.meta.url).pathname); +const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url))); const DOCS = join(ROOT, 'docs'); const CANONICAL_TOP_LEVEL_DIRS = new Set([ diff --git a/scripts/http-cache-security.test.mjs b/scripts/http-cache-security.test.mjs new file mode 100644 index 0000000..f687ee2 --- /dev/null +++ b/scripts/http-cache-security.test.mjs @@ -0,0 +1,91 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const consumer = process.env.READER_CACHE_CONSUMER || 'landing-astro'; +const consumerRequire = createRequire(path.join(root, consumer, 'package.json')); +const astroRequire = createRequire(consumerRequire.resolve('astro/package.json')); +const CachePolicy = astroRequire('http-cache-semantics'); +const request = { + url: 'https://cache-fixture.invalid/image', + method: 'GET', + headers: { host: 'cache-fixture.invalid' }, +}; +const cookie = { 'set-cookie': 'fictional-fixture=synthetic' }; +const scenarios = [ + ['shared cookie without explicit opt-in', 'max-age=0', cookie, true, 1, false], + [ + 'shared cookie with stale-while-revalidate', + 'max-age=0, stale-while-revalidate=600', + cookie, + true, + 1, + false, + ], + ['shared proxy revalidation', 'max-age=60, proxy-revalidate', {}, true, 61, false], + ['response requires revalidation', 'no-cache', {}, true, 1, false], + ['response cannot be stored', 'no-store', {}, true, 1, false], + ['private response in shared cache', 'private, max-age=60', {}, true, 61, false], + ['ordinary expired response', 'max-age=0', {}, true, 1, true], + ['ordinary fresh response', 'max-age=60', {}, true, 1, true], + ['explicitly public cookie', 'public, max-age=0', cookie, true, 1, true], + ['explicitly immutable cookie', 'immutable, max-age=0', cookie, true, 1, true], + ['cookie in private cache', 'max-age=0', cookie, false, 1, true], + ['proxy directive in private cache', 'max-age=0, proxy-revalidate', {}, false, 1, true], + ['must-revalidate remains enforced', 'max-age=0, must-revalidate', {}, true, 1, false], +]; + +for (const [name, cacheControl, extraHeaders, shared, age, expected] of scenarios) { + for (const serialized of [false, true]) { + test(`${consumer}: ${name}${serialized ? ' after serialization' : ''}`, () => { + let policy = new CachePolicy( + request, + { + status: 200, + headers: { 'cache-control': cacheControl, ...extraHeaders }, + }, + { shared } + ); + if (serialized) policy = CachePolicy.fromObject(policy.toObject()); + policy.now = () => policy._responseTime + age * 1000; + const next = { + ...request, + headers: { ...request.headers, 'cache-control': 'max-stale=99999' }, + }; + assert.equal(policy.satisfiesWithoutRevalidation(next), expected); + assert.equal(Boolean(policy.evaluateRequest(next).response), expected); + }); + } +} + +test(`${consumer}: max-stale still respects its age bound and request identity`, () => { + const policy = new CachePolicy(request, { + headers: { 'cache-control': 'max-age=10' }, + }); + policy.now = () => policy._responseTime + 15_000; + assert.equal( + policy.satisfiesWithoutRevalidation({ + ...request, + headers: { ...request.headers, 'cache-control': 'max-stale=4' }, + }), + false + ); + assert.equal( + policy.satisfiesWithoutRevalidation({ + ...request, + headers: { ...request.headers, 'cache-control': 'max-stale=6' }, + }), + true + ); + assert.equal( + policy.satisfiesWithoutRevalidation({ + ...request, + url: 'https://cache-fixture.invalid/other', + headers: { ...request.headers, 'cache-control': 'max-stale=99999' }, + }), + false + ); +});