Skip to content

fix: harden sanitization, CLI parsing, retries, and web robustness#293

Merged
jongio merged 5 commits into
mainfrom
fix/quality-hardening
Jul 19, 2026
Merged

fix: harden sanitization, CLI parsing, retries, and web robustness#293
jongio merged 5 commits into
mainfrom
fix/quality-hardening

Conversation

@jongio

@jongio jongio commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

A focused hardening pass across the SDK, CLI, and web packages. It closes a terminal control-character injection vector, fixes a CLI argument-parsing bug that could target the wrong device, makes retry backoff respect the server's Retry-After, and tightens several web error-handling paths. There are no changes to happy-path rendering or public API signatures.

Changes

SDK

  • Sanitize untrusted cloud free-text at the Firestore parse boundary. sanitizeLabel previously covered only device/channel labels; it now also strips ANSI/control characters from status, type, firmware, sessionLabel, notes, and the archive equivalents, which several CLI commands printed raw (CWE-150).
  • Honor Retry-After as a hard floor in computeRetryDelay: jitter only the exponential component and clamp to at least the server-requested wait (still capped at maxDelay), so a 429/503 is never retried early.
  • Fetch device-group documents in parallel instead of sequentially (order and 404 handling preserved).

CLI

  • Add a shared firstPositional(args, valueFlags) helper and use it in eta, temp, stall, alarm-suggest, and convert. The old args.find(a => !a.startsWith("--")) treated a value flag's value as the positional when the flag came first, so eta --target 203 ABC123 queried device "203".

Web

  • Add \r to the CSV formula-injection prefix set (parity with the CLI escaper).
  • Wrap the cook-annotations localStorage write in try/catch (matches the reader).
  • Replay each queued offline mutation independently so one failure doesn't block the rest.
  • Clear stale archive state on a failed or absent shared-archive fetch.

Docs

  • Correct the pnpm minimum version in CONTRIBUTING to match package.json (11.14.0).

Testing

All checks green locally:

  • build (all 5 packages), lint (biome, 425 files, 0 issues), typecheck (all packages)
  • sdk: 547 tests (+1 skipped), cli: 1194 tests, web: 68 files / 911 tests
  • New tests: CLI firstPositional, SDK Retry-After floor, SDK parse-boundary sanitization, web offline-replay resilience.

Playwright e2e/a11y/visual weren't run locally: every change is non-visual (escaping, try/catch, replay logic, error-path state, parse sanitization), so happy-path rendering and the committed visual baselines are unaffected.

Type of change

  • Bug fix (non-breaking)
  • Security hardening
  • Breaking change

Security

  • No secrets committed
  • Untrusted cloud free-text sanitized before terminal and CSV sinks
  • No new eval / innerHTML / dangerouslySetInnerHTML / exec
  • Dependencies unchanged (no new CVE surface)

Notes for reviewers

Surfaced during review and deliberately left for follow-ups (not in this PR): web account tokens in localStorage (backendless-SPA tradeoff), UserManagement invite removal targeting the users collection (needs the correct cloud endpoint), dialog focus-trap and list-semantics a11y, and MCP/watch function-size refactors.

jongio and others added 5 commits July 19, 2026 03:11
…p fetch

Extend the parse-boundary sanitizer to strip ANSI/control characters
from every attacker-influenceable free-text field (device/channel
status, type, firmware, sessionLabel, notes, and the archive
equivalents), not just labels. This closes a terminal control-character
injection vector (CWE-150) in CLI output where those fields were printed
raw.

Also fetch device-group documents in parallel instead of sequentially;
Promise.all preserves order and the existing 404 handling while avoiding
N sequential round trips.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed65448a-d217-4a10-996d-a873a14f6e32
computeRetryDelay documented Retry-After as a floor, but full jitter was
applied over max(exponential, retryAfter), so the delay could fall below
the server-requested wait. Jitter only the exponential component and
clamp the result to at least retryAfterMs (still capped at maxDelay), so
a 429/503 carrying Retry-After is never retried early. Adds regression
tests covering the floor and its cap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed65448a-d217-4a10-996d-a873a14f6e32
Commands resolved their positional (serial/meat/value) with
args.find(a => !a.startsWith("--")), which mistakes a value-taking
flag's value for the positional when the flag comes first, e.g.
`thermoworks eta --target 203 ABC123` queried device "203". Add a shared
firstPositional(args, valueFlags) helper that skips value flags and
their values, and use it in eta, temp, stall, alarm-suggest, and
convert. Adds unit tests for the helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed65448a-d217-4a10-996d-a873a14f6e32
…ror state

- export: add carriage return to the CSV formula-injection prefix set,
  matching the CLI escaper.
- cook-annotations: wrap the localStorage write in try/catch so a quota
  or private-mode error can't break the report UI, matching the reader.
- offline-mutations: wrap each queued replay in try/catch so one failing
  mutation no longer blocks replay of the rest (adds a regression test).
- SharedArchiveView: clear stale archive state on a failed or absent
  fetch so a previous archive can't linger on an invalid link.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed65448a-d217-4a10-996d-a873a14f6e32
Match the pnpm version pinned in package.json (11.14.0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ed65448a-d217-4a10-996d-a873a14f6e32
@jongio
jongio merged commit c651fe2 into main Jul 19, 2026
5 checks passed
@jongio
jongio deleted the fix/quality-hardening branch July 19, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant