Repository navigation
SupportOS v1.6.0 — the hardening release (second neutral audit)
SupportOS v1.6.0 — the hardening release
A second full neutral audit — three independent adversarial passes (server core, data/sync layer, React client) plus a human-like usage pass — found 2 HIGH + 28 MEDIUM issues. All fixed, each with regression coverage. No new features: v1.6.0 makes everything that already shipped behave the way it already claimed to.
Highlights
HIGH — fixed
- Outreach send-queue livelock: recipients that exhausted 3 retryable attempts stayed
queued— unclaimable but still counted — sosendBatchre-enqueued itself forever and campaigns could never complete. Exhausted rows are swept tofailed; Retry failed now resets the attempt budget (it was a silent no-op). - Docs embedding churn: every 5-minute incremental sync re-chunked EVERY article, destroying all stored embeddings even when the text was byte-identical — permanent re-embedding, permanently lagging semantic search. Content-hash-gated re-chunking now (migration 010).
Silent-dead features — verified live before/after
- The entire v1.4.0 webhook-push client UX never fired: the browser's EventSource never subscribed to the server's
conversationevent — one missing word in a listener list, invisible to tests that asserted the wire event instead of the toast. - Write-behind writes never told anyone they landed (found in the human-like browser pass): bulk ops ack "queued", the client invalidates, races the worker, reads stale state. The worker now emits
conversation-updatedafter each completed write — an externally-applied tag appears in an open conversation view within ~2s. - Error states on every flaggable query, onError toasts on ~20 silent mutations, Settings forms gate on loaded data (were silently overwriting real config with defaults), 300ms debounce on the per-keystroke audience preview,
safeExternalHrefallowlist againstjavascript:hrefs, toast cap, keyboard-operable inbox rows.
Server input hardening — every crash reproduced live before fixing
- Rate limiter bypass closed: it keyed on spoofable
X-Forwarded-For(310/310 requests passed with header rotation); now keyed on the socket address. - Six endpoints 500'd on NaN query params; seven routes crashed on missing bodies; numeric
name/text500'd campaign create + simulate-incoming; a 2MB search query crashed FTS5; queue retry/cancel lied withok:true— all clean 4xx responses now, each with an e2e test.
Data/sync correctness
- Incremental sync coverage gap: organizations + property definitions only synced during initial sync — post-initial creations never appeared, so segmentation conditions on them could never match.
- The ISO-vs-
datetime('now')date-comparison bug class (12+ sites, up to 24h boundary skew): segmentation date windows, issue trends, reply detection, retention pruning — all normalized tojulianday(). - The satisfaction.ratings webhook never stored ratings on the real provider (read a phantom field); same-millisecond local tag collisions threw UNIQUE; soft-deleted customers never resurrected; FTS ghost rows outlived their threads; backups ran 4x faster than configured and never pruned; failed embedding chunks retried forever; approval jobs were silently completed as no-ops.
Numbers
- Tests: 259 → 288 (29 new regression tests; every finding with a reproducible failure mode is locked by a named test)
- 320-check black-box audit script re-run against the fixed build: 0 findings
- Human-like browser pass: webhook push toast + live list refresh, bulk tag appearing in an open conversation within one worker tick, full reply flow, every page walked with zero console errors
See the CHANGELOG for the complete list and the README for the story and the decision log.
Desktop installers (MSI / NSIS / DMG / AppImage) are built by the tag-triggered workflow and attached below when ready.
⚠️ Upgrading from v1.5.x: migration 010 applies automatically and idempotently on first boot (addsdocs_articles.content_hash+ embedding attempt counters). Nothing else changes on disk.