Address round-1 review feedback on fetchTracking redaction:
- Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader
understands why two identical "[REDACTED]" constants coexist.
- Document that redactBody deliberately scopes to form-encoded + JSON
(multipart/binary bodies pass through; OAuth never uses multipart).
- Add a tracker-path test asserting a nested sensitive key in a JSON
request body is redacted in the recorded copy while the live outbound
body stays byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Broaden the web vitest `coverage.include` from the single-file
`src/lib/sandbox-csp.{ts,tsx}` entry to `src/lib/**/*.{ts,tsx}` so the
per-file >=90 four-dimension gate applies to every library module under
`src/lib`, not just sandbox-csp.
- Add unit tests for the previously-ungated `environmentFactory.ts`
(createWebEnvironment) and `types/customHeaders.ts` value helpers,
bringing both to 100% on all four dimensions.
- Annotate downloadFile.ts's provably-dead `?? ""` fallback (String.split
always returns a non-empty array so .pop() is never undefined) with a
justified `v8 ignore`, closing its last uncovered branch (92.3 -> 100).
All five src/lib files now report 100/100/100/100.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Extends the header redaction from #1585. Before a fetch entry reaches any
sink, mask known-sensitive credentials in the request/response bodies and
the URL query string, in addition to headers:
- redactBody(): form-urlencoded + JSON bodies, recursive over nested
objects/arrays, best-effort (never throws), values-only.
- redactUrlQuery(): masks sensitive query params, preserves path,
non-sensitive params, and any fragment.
- Sensitive fields: client_secret, code, refresh_token, access_token,
id_token, code_verifier, client_assertion, assertion, password, token
(case-insensitive), reusing the [REDACTED] sentinel.
Applied to the request success + error entries and the async response-body
callback. Live outbound requests are byte-identical — only the logged copy
is redacted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
The "cancels a pending debounce timer when the input is re-focused" test
passed trivially after the delay:null change: synchronous typing meant the
assertion ran at t≈0, before the 300ms debounce could ever fire, so it
passed whether or not handleFocus's clearTimeout worked.
Rewrote it with fake timers so the debounce window elapses deterministically
(advance the clock by 400ms directly). The interaction is driven with
fireEvent rather than userEvent because userEvent's async internals deadlock
under vitest fake timers with the Mantine/happy-dom stack. Verified
load-bearing: removing the clearTimeout in handleFocus makes it fail
(expected 1 to be 0), and it passes once restored.
Also tightened the sibling-context comment (with delay:null the sibling is
reliably "es"); the tolerant /^es?$/ regex is left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.
Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.
Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
past the 300ms completion debounce (await setTimeout(400)) — which race
the debounce timer under load — with awaited conditions (findBy/waitFor)
on the real rendered outcome, plus delay:null. Negative assertions now
lean on the component's synchronous state/timer teardown instead of a
timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
piped out-of-band from the tool's JSON-RPC response, so reading the log
synchronously after callTool raced the stderr chunk. Wrapped the assertion
in vi.waitFor so it polls until the line lands.
Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Completes requirement #2 of #1517: a table mapping each active version to
its base branch, project board, and version label, plus a "Label by
version" note mirroring AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Round-3 @claude review of #1592 (optional nit): getClientRegistrationKind is
the other synchronous getter that, like getScope, is safe only because its
sole caller (buildOAuthConnectionState) awaits getClientInformation/
getServerMetadata first, flushing hydration before this runs on the
post-redirect callback path. Add a docstring mirroring getScope's so a future
refactor doesn't read it without a preceding awaited storage read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Round-2 @claude review of #1592 caught a seventh member of the clear
family that the first pass missed: clearClientInformation still wrote to
the store synchronously, carrying the same pre-hydration clobber risk the
clearAfterHydration helper was written for (a clear before hydration lands
persists a near-empty blob, overwriting the whole on-disk oauth.json).
Wrap its mutation the same way and add a clear-before-hydration test
proving a sibling token (and thus every other server's blob) survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-3 code-review note on PR #1587: resource `_meta` is applied
only by the default read handler; a customHandler from onRegisterResource
replaces the contents wholesale and would drop it. Document this on
ResourceDefinition._meta so a future App UI resource with a custom read
handler knows to re-add _meta itself.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
The root package.json engines.node was bumped to >=22.19.0 but the root
package entry in package-lock.json still declared >=22.7.5. Regenerate so
the lockfile matches package.json for npm ci.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-1 @claude review of #1592:
- OAuthStorageBase.clear* no longer mutate the pre-hydration (empty) store —
a clear issued before hydration is deferred until it lands, so it merges
onto the real persisted state instead of (a) being resurrected by the late
rehydrate merge or (b) persisting a near-empty blob that clobbers every
other server's on-disk credential. Already-hydrated clears stay synchronous.
- store.setServerState migrates a pre-normalization raw-key blob onto the
canonical key on first partial write (was: shadowed it with a fresh
canonical entry, orphaning the raw blob's other fields). clearServerState
now also drops the raw-key orphan.
- Document the keepalive 64KB combined-body ceiling on the remote-storage
write, the getRemoteOAuthStorage memo key intentionally omitting fetchFn,
and the real ordering invariant behind the sync getScope.
- Tests for the clear-before-hydration and raw-key-migration paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-2 code-review note on PR #1587: the _meta passthrough was
wired only into the regular registerTool path, so an App-flavored task
tool would not surface tool-level _meta. Add _meta to TaskToolDefinition
and a matching conditional spread on both registerToolTask overload
configs so the two registration paths behave symmetrically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-1 @claude review of #1588 (no behavior change to the
enforced policy):
- approveCspSources / buildSandboxCspPolicy: document that
resourceDomains intentionally feeds script-src/style-src per the
McpUiResourceCsp contract, and that approve screens injection-safety
only (not breadth — a bare `*` is accepted; safe under the
opaque-origin sandbox).
- CSP_KEYS: replace the `satisfies` (proves listed keys valid) with an
exhaustiveCspKeys() helper that ALSO fails to compile if the upstream
ext-apps type gains a domain key CSP_KEYS omits, so a requested
restriction can never be silently dropped.
- SAFE_CSP_SOURCE: accept case-insensitive schemes (URL schemes are
case-insensitive); previously over-rejected `HTTPS://…`. Adds tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
undici 8.5.0 requires Node >= 22.19.0 (its declared engines), so CI's
Node 20.x could not load undici's CacheStorage (missing
webidl.util.markAsUncloneable) and the proxy integration tests failed.
Bump the CI setup-node to 22.x to match the repo's Node floor.
Reconcile the declared floor with undici's real requirement: bump root
engines.node to >=22.19.0 and correct the CLI README note (undici 8.5.0
needs >=22.19.0, not >=22.7.5). Also document proxy support in the web
README and clarify that a caller-supplied eventSourceInit.fetch bypasses
the proxy wrapper by design.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-1 review nits on apps.ts:
- readUiMeta now accepts `unknown` and narrows internally, removing the
three `as WithUiMeta` casts at its call sites (readability win).
- Add a comment documenting that the content-block-then-result precedence
for `_meta.ui` is intentional (posture lives on the content block; no
shallow merge).
- Add a TODO to switch to the named McpUiToolMeta/McpUiResourceMeta types
once the upstream extensionless re-export is fixed for NodeNext.
Coverage: apps.ts 100% lines / 97.05% branch / 100% funcs — clears the gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
- Soften the OAuth-callback-rejected toast: the shape guard is
defense-in-depth, not full CSRF prevention (PKCE remains primary), so the
message no longer overstates ("rejecting to prevent a cross-site request" ->
"did not originate from this session").
- Document the intentional asymmetry between a present-but-malformed `state`
(rejected) and a wholly absent `state` (accepted, matched via
OAUTH_PENDING_SERVER_KEY) — rejecting the null case would mask real provider
error redirects that omit `state`.
- Make the "valid state is not rejected" test assert the specific downstream
"could not be matched" toast (seeding OAUTH_PENDING_SERVER_KEY) instead of an
indirect "some toast fired" check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address code-review DRY note on PR #1587: MCP_APP_DEMO_URI was defined
in both the fixture and the integration test. Export it from the fixture
and import it in the test so the two stay in lockstep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX