fix(web-ui): upgrade next to 16.3 to clear four high-severity advisories (#1124) - #1132
Conversation
…ies (#1124) Four high-severity advisories were live in web-ui. The PR that would have fixed them (#1087) was closed by Dependabot itself as "updatable in another way", and its replacement (#1123) bumped only top-level postcss — which was never the exposure. The vulnerable copy was nested at next/node_modules/postcss, pinned by next 16.2.12. next 16.3.0 clears all four: GHSA-r28c-9q8g-f849 postcss path traversal via previous-source-map loading GHSA-6g55-p6wh-862q postcss arbitrary file read via sourceMappingURL GHSA-qx2v-qp2m-jg93 postcss XSS via unescaped </style> in stringify GHSA-f88m-g3jw-g9cj sharp inherited libvips CVEs (< 0.35.0) npm audit: 7 vulnerabilities (6 high) -> 4 (3 high). sharp 0.34.5 -> 0.35.3, and the nested postcss is now 8.5.23, past every advisory's patched version. The upgrade brings stricter bundled typings, which broke the build with 27 TypeScript errors — all in src/__tests__, none in production source. Fixed rather than silenced; no `as any` was added anywhere (two existing ones were narrowed to `as never`), and all 1276 tests still pass: - reviewApi.getFileDiff does not exist. The test mocked and stubbed a method that is not on the API; the `as jest.Mock` cast hid it. Removed — dead setup, and its 13 tests still pass. - ProofRequirement.scope became a required (nullable) field with the #568 scope work; five fixtures predated it and now supply it. - TaskBoardView called .mockResolvedValue on the real typed tasksApi. Now uses jest.mocked(), which keeps the real signatures — so the toHaveBeenCalledWith assertions still type-check against true argument shapes. That immediately caught two more: updateStatus.mockResolvedValue({}) where a Task is required, now using the file's own makeTask factory. - Blocker.created_by is required; the fixture omitted it. - The WebSocket mock declared onclose as taking no arguments while calling it with a CloseEvent, and passed Uint8Array.buffer (ArrayBufferLike, possibly a SharedArrayBuffer) where a real binary message carries an ArrayBuffer. - Two @ts-expect-error directives are now unused because axios types `handlers`; removed, with the reason kept as a comment. - axios.isAxiosError was being reassigned, which fights jest.Mocked's intersection type; drives the existing auto-mock instead. - The remaining partial mocks of SWRResponse / AppRouterInstance assert through `unknown`, which keeps the target type rather than erasing it. #1123 is already merged and is harmless but unrelated. The four remaining advisories (all dev/build-time) and raising deploy.yml's --audit-level above critical are coupled and tracked in #1131 — raising the gate today would block every deploy on findings nothing has decided about yet.
|
Claude finished @frankbria's task in 3m 49s —— View job 🔍 Precision bug-hunt review complete
Verdict: no defects found. Formal summary posted as a separate comment below. What I verified against the real source (not just the diff): the diff is |
|
Reviewed the diff (package.json/lockfile bump + 13 test-file fixes, no production source changes). Verified claims
Minor, non-blocking nits
Not addressed here (and correctly deferred): the four remaining dev/build-time advisories and raising No bugs, security concerns, or missing test coverage found. The typing fixes look like genuine drift/defect fixes rather than suppression, and the evidence table in the description (build, tsc, test, audit counts) is consistent with the diff. LGTM. |
✅ GLM precision review: no defects found
Scope checked — every hunk read against surrounding source, not the diff alone:
Nothing to fix. Security-wise this is a net improvement (four high advisories cleared). |
…atting (#1124) Both non-blocking, both worth taking: the jest.mocked() binding sat between two import statements (valid, but an import/first rule would flag it), and scope: null was appended to the related_reqs line rather than getting its own, which did not match the surrounding fixture style. No behaviour change: tsc clean, eslint clean, 108 tests in the touched suites still pass.
|
Took both nits — they were fair:
No behaviour change: Also — thanks for independently verifying rather than taking the description on trust, particularly grepping the diff to confirm the |
|
Claude finished @frankbria's task in 2m 57s —— View job ✅ GLM precision review: no defects found (re-run on synchronize
|
| severity | file:line | finding |
|---|---|---|
| — | — | no concrete defects (logic / security / data-loss / race / contract / runtime-type) found |
Re-ran on the new nit-fix commit and re-verified the full diff against the checked-out source (not the diff alone). The new commit eec2792 is purely cosmetic — scope: null moved to its own line in the two proof fixtures, and import type { Task, TaskListResponse } moved above the jest.mocked() comment — zero runtime surface, no behavior change.
What I confirmed against real code this pass:
auth.test.ts—isAxiosError.mockReturnValue(true)vs the old property reassignment.beforeEachusesjest.clearAllMocks()(lines 59, 103), which clears call history but does not resetmockReturnValueor undo a=reassignment — so once set totrue, both forms persist identically across subsequent tests. No test orders a "needs-falsy-isAxiosError" case after the point it's first set true: thelogin/registersuccess-path tests (lines 62, 106/118/129) never reject, so the catch branch that callsisAxiosErroris never reached. Behavior is equivalent; no regression introduced.TaskBoardView.test.tsx—updateStatus.mockResolvedValue(makeTask(...))vs{}. Both call sites (lines 262–276, 369–394) assert onlytoHaveBeenCalledWith('/test', 't6', 'READY')andmockMutate.toHaveBeenCalled()— neither reads the resolved value.makeTask(line 41) yields a more completeTaskthan{}, so this can only be safer.jest.mocked(tasksApi)resolves against the hoistedjest.mock('@/lib/api')factory; pure typing, same mock objects at runtime.useTerminalSocket.test.ts— signature changes. Bothonclosecall sites (close()line 43,simulateClose()line 62) pass{ code }matching(event: { code: number }) => void; the lonesimulateBinaryMessagecaller (line 152) passesnew Uint8Array([...])whose inferred type isUint8Array<ArrayBuffer>, andbytes.bufferis anArrayBuffermatchingonmessage's{ data: string | ArrayBuffer }. Runtime unchanged.ResolvedBlockersSection+CaptureGlitchModal/ProofPagefixtures. Verified againstsrc/types/index.ts:Blocker.created_by: BlockerOrigin(line 140, required) andProofRequirement.scope: ProofScope | null(line 381, required-nullable) — both real fields, both satisfied by the added values.page.test.tsx—key: unknown+typeof === 'string'is a stricter guard than the oldkey: string | null+ truthiness check (rejects non-string truthy keys too);as never/as unknown as Tescapes return real{data,error,isLoading,mutate}objects whose consumed members are all present. Type-only.package.json+ lockfile —next^16.2.12→^16.3.0, transitivesharp→0.35.x and nestedpostcss→8.5.23 clear the four target advisories. No stray pin or version conflict.
Net security change is positive (four high advisories cleared); no production src/ changed. Nothing to fix.
|
Re-reviewed after the Confirmed fixed:
Re-verified independently:
Nothing new to flag. This is a clean, well-scoped dependency bump with test-only typing fixes — no behavior change, no |
Closes #1124.
What was actually exposed
Four high-severity advisories were live. The PR that would have fixed them
(#1087) was closed by Dependabot itself — "updatable in another way" — and
its replacement #1123 bumped only top-level postcss, which was never the
exposure. The vulnerable copy was nested at
next/node_modules/postcss, pinnedby
next16.2.12.next16.3.0 clears all four:GHSA-r28c-9q8g-f849GHSA-6g55-p6wh-862qsourceMappingURLGHSA-qx2v-qp2m-jg93</style>in stringifyGHSA-f88m-g3jw-g9cj< 0.35.0)Each of the four verified individually as absent from the post-upgrade audit.
The 27 type errors — fixed, not silenced
The upgrade brings stricter bundled typings. All 27 were in
src/__tests__,none in production source. No
as anywas added anywhere — two pre-existingones were narrowed to
as never— and all 1276 tests across 104 suites stillpass.
Two of them were real defects rather than typing noise:
reviewApi.getFileDiffdoes not exist. The test declared it in the modulefactory and stubbed it in
beforeEach. There is no such method onreviewApi;the
as jest.Mockcast hid it. Removed as dead setup — its 13 tests still pass.TaskBoardViewmocked with the wrong types. It called.mockResolvedValueon the real typed
tasksApi. Switching tojest.mocked()keeps the truesignatures, so the
toHaveBeenCalledWithassertions still type-check againstreal argument shapes — and that immediately surfaced two more:
updateStatus.mockResolvedValue({})where aTaskis required. Now uses thefile's own
makeTaskfactory.The rest were genuine drift or stricter typings:
ProofRequirement.scopebecame a required (nullable) field with the [Phase 3.5C] Glitch Capture: entry point and capture form #568 scopework; five fixtures predated it
Blocker.created_byis required; one fixture omitted itoncloseas taking no arguments while calling itwith a
CloseEvent, and passedUint8Array.buffer(ArrayBufferLike, i.e.possibly a
SharedArrayBuffer) where a real binary message carries anArrayBuffer@ts-expect-errordirectives are now unused because axios typeshandlers; removed, reason kept as a commentaxios.isAxiosErrorwas being reassigned, which fightsjest.Mocked'sintersection type; now drives the existing auto-mock
SWRResponse/AppRouterInstanceassert throughunknown, which keeps the target type asserted rather than erasing itEvidence
npm run buildtsc --noEmiterrorsnpm testnpm auditas anyaddedAcceptance criteria
next≥ 16.3.0 inpackage.jsonand the lockfilenpm run buildexits 0npm testpasses, with the fixes preserving what the tests assertThe two "also worth deciding" items → #1131
The remaining four advisories (
brace-expansion,js-yaml,nanoid,@babel/core— all dev/build-time) and raisingdeploy.yml's--audit-level=criticalare coupled, so I filed them together as#1131 (P2.35) rather than half-doing either here: raising the gate to
hightoday would immediately block every deploy on three findings nothing has decided
about yet. The right order is decide, then raise.
Worth remembering
Dependabot closed a security PR and substituted a materially weaker one that
looked like the same fix. An open Dependabot PR is not evidence that a class of
advisory is being handled — #1123 was green, plausible, and did not touch the
vulnerable dependency.