feat: @stuajnht's feature requests + team-scoped palette, and the mention-rendering bug they uncovered - #496
Conversation
The palette filtered commands by GLOBAL access rules only, so a user whose team holds a create-capability grant (and no global *.manage rule) never saw "Create Incident" / "Create Maintenance" or their shortcuts - it hid the actions from exactly the people authorized to run them. Commands may now declare `manageCapability`, mirroring the gate routes/nav use. filterByAccessRules admits an item on the global rules OR a team grant on the declared type; the backend resolves that per request via hasAnyTypeGrant (includeCreator, so a would-be creator qualifies before owning an instance) and fails closed. Incident + maintenance commands declare their types. useGlobalShortcuts drops its userAccessRules re-check: the server-filtered list is authoritative, the check tested global rules only, and both call sites already defeated it by passing ["*"]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
From user feedback on the "Who can change this" editor: - Relabel the "Manage" checkbox to "Can edit" and drop its gear icon. It sets the team's grant on THIS resource, but the wording + gear read as "manage the team", so people expected it to open the Teams UI. - Make the team name a link to /teams?team=<id>, which opens that team's members dialog. "Go to the team" now has its own affordance instead of being conflated with the grant checkbox. TeamsTab consumes the param once, then clears it. - Confirm before a user revokes their OWN team's only edit grant: afterwards they could neither change the resource nor restore the permission. Global teams.manage admins are exempt (they can always restore). The decision is extracted as the pure, unit-tested isSelfRevokingChange. - Explain the add-member field: it adds a NEW member from the directory (not a filter over current members), and only users who have signed in at least once exist there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
🦋 Changeset detectedLatest commit: a05dcf1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 165 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
❌ PR Checks Failed
❌ Security Audit Failures (dependency graph)How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run @enyineer The above code quality issues were found in this PR. Please fix them before merging. |
The system overview listed a system's incidents/maintenances but gave no way to open one for it - you had to go to the incidents page and re-pick the system. Both system panels now offer "Report incident" / "Schedule maintenance", deep -linking to the editor with the system pre-selected via ?action=create&systemId=<id>. The config pages consume and clear both params. The action is gated on useProcedureAccess over the CREATE procedure's contract, so it shows for a global manager AND for someone who manages the system via a team (create.parent) - the bare global rule would have hidden it from exactly the team-scoped users it helps most. Editors gain `presetSystemIds`, and their unsaved-changes baseline accounts for it so opening a pre-scoped form and closing it does not nag to discard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
❌ PR Checks Failed
❌ Security Audit Failures (dependency graph)How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run @enyineer The above code quality issues were found in this PR. Please fix them before merging. |
Access-rule categories now sort by their rendered label instead of plugin registration order, at both the category and rule level. Each category gained Select all / Clear, routed through the same guard the individual checkboxes use so the bulk action cannot grant a rule the anonymous role may not hold. Roles can be cloned. The dialog takes an explicit mode rather than inferring 'editing' from a role being present, which is what made the third state expressible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A Clone row action opens the editor seeded from the source and saves as a create. The clone is deliberately SHALLOW: name (suffixed), description and custom fields only. Memberships, links, team grants and health-check assignments are not copied, and the dialog says so - duplicating check assignments would silently multiply probe volume with every clone. Gated on CREATE rather than manage of the source, and a GitOps lock on the source does not block it: the copy is a new, unmanaged record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
With exactly one leading maintenance window or active incident, the system overview card now shows its title linked to the record instead of the count. A bare '1' told the reader nothing they could not infer from the card being there, and forced a second click. With two or more there is no single thing to name, so the count remains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
The theme control is now Light / Dark / Auto. Auto persists 'system' and follows the operating system. Fixes two bugs. Auto was a one-way door: the backend, schema and ThemeProvider had always supported 'system', but both toggles were binary and could only write light or dark, so touching the control once destroyed the preference with nothing able to write it back. And Auto did not react: ThemeProvider read matchMedia during render with no listener, so a live OS switch did not repaint until something unrelated re-rendered. Resolution is now a pure function, and a value read from localStorage is narrowed rather than cast so a hand-edited value cannot reach the html class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
The About page showed only the backend package version, which cannot be matched to a GitHub release, a Docker tag or a changelog entry - those carry @checkstack/release's version, which advances on every release while the core package's does not. Both are now shown, labelled, with the release version leading and linked to its tag. It is baked in at version time by a new generate:release-version script (checked in CI) rather than read at runtime: @checkstack/release is private and so is absent from node_modules in an npm install, where a relative-path read would silently fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…fications A maintenance notification said only that something had been scheduled, which told a subscriber nothing about what was planned or when - every recipient had to open the app to learn anything. The body now carries the scheduled window and the description the operator had already written. The window renders in UTC with an explicit suffix: the pipeline has no per-recipient timezone, so a server-local time would be silently wrong for most subscribers and an unlabelled one unfalsifiable. The description goes through the same sanitiser as an update message, so authored markdown survives while control characters and blank-line padding do not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A System picker sits beside the environment one, so {{ system.metadata.<key> }}
resolves in the preview line and offers autocomplete.
Previously system templating could only be previewed when the editor happened to
be opened FROM a system: a shared-config authoring flow and every edit-mode
session got no preview and no completions at all, because the systems list was
not even fetched in edit mode.
Selecting only a system is now enough - an environment is no longer required,
since system.metadata.* resolves without one. Both pickers only offer resources
the caller may read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Markdown fields were plain textareas with a hint, so an author found out how their text rendered only after saving - or, for a notification, after it had been delivered. MarkdownEditor adds Write / Preview tabs and a toolbar, adopted by the incident and maintenance update forms and descriptions and the announcement message. The preview renders through MarkdownBlock - the same component, remark/rehype chain and sanitiser as the saved content. A second renderer would be free to drift, and a preview that disagrees with the real render is worse than none. Toolbar marks toggle rather than only adding, and mark lengths are matched exactly so italic never claims bold's delimiters and silently downgrades an author's emphasis. Note for adopters: the component wraps its textarea, so 'required' on it does nothing - gate submission explicitly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Status-update dots were uniformly grey, so the rail carried no information. Maintenance dots take the update's own status: maintenance has no severity, so its lifecycle is the one coloured dimension. Incident dots take the incident's SEVERITY, keeping status on a neutral pill - incidents carry both an urgency and a lifecycle, and status-tone.ts gives the hue to the urgency, so colouring both would put two competing scales on one row. Public status pages now tone the dot to match the status label already beside it. An update that changes nothing stays neutral, so a coloured dot always means the status moved here. Also fixes the rail: it anchored its left EDGE at left-4, putting its centre at 16.25px while every dot centres at 16px. A new TimelineDot owns the positioning so the four copies of that maths cannot diverge again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Adds optional proxy URL, username and password, so a network requiring an
outbound proxy can be monitored through the path its users take.
The proxy becomes the egress policy boundary for that check: the SSRF denylist
is applied to the PROXY host, because that is the only host we connect to, and
the target is left for the proxy to resolve. A filtering proxy is frequently the
only thing that can resolve the target, so pre-resolving locally would reject
valid checks while proving nothing about the real egress. A proxy pointed at a
denied range is still refused.
Connect/TLS timings are omitted for proxied checks: the probe that measures them
opens a raw socket to the resolved target, a path a proxied request never takes.
The password is a secret field - encrypted, redacted, resolvable as
${{ secrets.NAME }}, delivered to satellites just in time. It is deliberately
NOT templatable: a field marked both secret and templatable is rejected at
plugin load, so the combination would break boot. Tests pin each field's
contract.
A proxy answering with an error is a COMPLETED request: 407 and 502 surface as
an assertable statusCode, not a transport failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…stop silent checks A satellite going offline was invisible, and so were its checks. Per-satellite threshold. The 45s global constant becomes a per-satellite override (2 minutes to 24 hours): tolerance is a property of the link, not the platform, so a flaky uplink should not force its grace on everything else. The threshold is carried on every row read by computeStatus, so the entity read, the admin list and the heartbeat monitor cannot disagree. Additive nullable column. Connectivity notifications. Satellites are a notification target with a subscription: a warning when one stops heartbeating, informational when it returns. A reconnect only notifies if it was actually offline, so a redeploy is not an event. BUG FIX: satellite-only checks no longer go silent. A check with includeLocal false whose satellites were all offline recorded NOTHING, so it displayed its last known status indefinitely - a dead probe was indistinguishable from a passing one. The core now records a degraded run. Degraded rather than unhealthy because the target may be fine; what failed is our ability to observe it. Liveness that cannot be resolved is treated as executing, so a transient lookup failure cannot mark the fleet degraded. Checks also surface staleness, and a RETIRED slice - environment removed or satellite unassigned - is never stale: warning about something you retired on purpose trains operators to ignore the badge. Corrects the user guide, which claimed offline satellites produced failed runs; they produced nothing at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Typing # in a markdown field opens a picker and inserts a reference. Referencing another record previously meant pasting a URL, which cannot be right everywhere: an admin URL is meaningless on a status page, a status-page URL is meaningless in the admin UI, and neither works in an email. A mention therefore stores WHAT it points at, never where: [Database upgrade](checkstack:maintenance/<id>). That is an ordinary markdown link - readable in the source, parsed unchanged by existing tooling - and only the href is resolved per context. Resolution may REFUSE: a resolver returning nothing renders the label as plain text rather than a link. That is a confidentiality property, not a nicety - an internal-only incident referenced from a public update must not become a link that confirms it exists. A renderer given no resolver links nothing. Detail pages gained a Referenced items section, derived by scanning the authored markdown on each render, so nothing is stored twice and an edit that drops a reference drops it from the list. The platform owns the contract; each owning plugin registers its own type, so no plugin imports another. Search only offers records the caller may read. Scope: wired for the admin UI. Public pages and notification bodies render mentions as plain text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Generated artefacts for the changes above: the bundled docs index (CI checks it is in sync with docs/src/content/docs), the lockfile and tsconfig references for the new workspace dependencies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
New specs for the markdown editor (preview tab, toolbar, # picker, mention resolution, Referenced items) and catalog cloning. The keyboard test is a regression guard: trigger detection re-ran on every keyup, including arrow keys the open picker had already handled, and reset the highlight - so the picker could not be navigated and Enter always inserted the first suggestion. Updated theme.spec and announcement.spec for behaviour these changes altered. The theme spec had been seeding localStorage, which ThemeSynchronizer overwrites for a signed-in user; it only passed before because the backend default resolved to the same colour. Hardened two flakes. The status-page Add button is disabled until a type is picked, so the real failure was the Select click not landing and the timeout surfacing one step downstream; the retry only opens the popover when it is not already open, since the builder's live-preview re-render can leave it open and blindly clicking would toggle it shut. The team row lags its refetch because the success toast fires first. Also updates four rlac specs for a placeholder renamed in an earlier commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
`heartbeat-monitor.it.test.ts` creates its own `satellites` table rather than running migrations, so a column added to the schema has to be added here too - `listConnectionLiveness` now selects `offline_threshold_ms` and the query failed with "column does not exist". The file's own comment already warned about this trap after `capabilities` did the same thing. The integration lane is gated behind CHECKSTACK_IT and excluded from `bun run test`, so this was green locally and would have failed CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…s offline One offline satellite degrades EVERY check assigned to it in the same tick, and healthy -> degraded classifies as an escalation, so a satellite hosting 50 checks produced 51 notifications for a single root cause. persistRunAndReact gains suppressSubscriberNotification, set for the unobservable-run path. The run, the transition and the health state are still written - the dashboard and the check's history stay accurate - and only the per-check alert is withheld. The satellite's own connectivity subscription already reports the cause once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…orm cache The health-check executor asks getOnlineSatelliteIds() on every tick of every satellite-only check, and the read behind it is a full scan of the satellites table over a cross-plugin RPC. The procedure previously had no live caller, so this was a newly introduced per-tick cost that scales with the number of such checks - the pattern .claude/rules/optimization.md calls out. Cached with a 5s TTL on the SHARED cache, never a pod-local Map: two pods must not disagree about whether a check is being executed. A TTL rather than explicit invalidation because liveness is a function of elapsed time, not of a write - there is no mutation to hang an invalidation off. 5s sits an order of magnitude below the smallest offline threshold the schema allows, so a cached answer can lag a transition by one tick (which the next tick corrects) but never span one. A cache failure degrades to the uncached answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Documents that the degraded runs from an offline satellite deliberately do not alert per check, that a retired slice is never stale, and the liveness cache's TTL reasoning. Regenerates the bundled docs index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
react-markdown blanks any href outside its safe-protocol list BEFORE the rehype plugins run, so `checkstack:<type>/<id>` reached the anchor renderer as an empty string. The renderer then saw no mention, took the ordinary-link branch, and emitted an <a> with no href - the label rendered, the link did not. Inline mentions were therefore inert in EVERY context since they shipped: the admin incident and maintenance detail pages as well as public surfaces. Nothing threw and no text was missing, so the failure was invisible. Two filters had to be widened, since either alone still drops the href: urlTransform now passes the mention scheme through (deferring everything else to defaultUrlTransform, so javascript:/data: stay blocked), and the sanitizer's protocol allow-list gains the same scheme. Markdown.mentions.test.tsx pins the whole path from authored markdown to a rendered anchor, including that an unresolved mention stays plain text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
useMentionResolution({documents}) collects the references a page is about to
render and asks each owning plugin, in ONE batched request, which of them the
viewer may read. A mention to a deleted or unreadable record now renders as
plain text instead of a link to a not-found page or an access gate.
Fails closed: while the check is in flight, and for any provider that cannot
answer, every mention renders as plain text. The label always shows either way.
The batch is capped at the bound the resolving procedures declare - over it the
request would be rejected wholesale and, because the resolver fails closed,
EVERY mention on the page would silently drop to plain text.
Adds rewriteMentions() for delivery contexts that cannot host a link at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
resolveIncidentRefs / resolveMaintenanceRefs take ids and return only those the caller may READ, carrying the same listKey post-filter as their list procedures so they can never be more permissive. Deliberately NOT a filter over the authoring search list: that list is shaped for browsing (it hides resolved incidents, and pagination would hide more), so a reference missing from it is not evidence the caller cannot read it. The IT tests pin exactly that case. They return ids and nothing else - no titles, no statuses - so an unreadable record is indistinguishable from a deleted one. The label already lives in the authored markdown. The detail pages and update timelines now resolve through the viewability check rather than mapping any well-formed reference to a route. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A public page now links a `#` reference when - and only when - the same page publishes the target, which is exactly the anti-enumeration gate its detail pages already apply. So "caused by #Database upgrade" becomes a working link, while a mention of an internal-only incident stays plain text rather than a link that would confirm it exists. Widgets opt in by declaring a mentionType, so the status-page packages take no dependency on any domain plugin. The constant lives in each plugin's *-common because both halves must agree - drift silently stops public mentions resolving. The custom-domain allow-list is extracted to public-api-paths.ts and bound to the contract: a host 404s anything not listed, so an omission breaks that procedure on customer domains ONLY, invisibly. It is now a compile error. BREAKING CHANGE (behavioural): the in-app public status page at /statuspage/view/<slug> now builds detail-page hrefs. It previously passed none, so item titles rendered as plain text there while the same page on a custom domain linked them. Both now behave identically. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
`checkstack:` is an internal scheme that notification channels do not understand, and each leaked it differently. Measured against the real converters, before this change: email -> <a>Database upgrade</a> (href stripped, dead anchor) text -> Database upgrade (correct by luck) slack -> <checkstack:maintenance/9f1c-abc|Database upgrade> Slack showed the internal URI to the recipient; Discord, Telegram and Teams render markdown natively and would have passed it through too. sanitizeUpdateMessage now flattens every mention to its label before the body reaches any channel, so no channel has to know the scheme exists. Flattening happens BEFORE the length bound, so the excerpt budget is spent on visible text rather than on an internal URI. Linking instead would need a per-recipient URL and, to be correct, a per-recipient permission check inside a fan-out that has neither. The notification already deep-links to the item it is about. mention-leak.test.ts asserts across all three converters - a composition test, because neither the sanitizer's nor the converters' own suites can catch this alone. The notification tests drive the real notifyAffectedSystems, since a caller that interpolated the message directly would pass both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
22 assertions across 9 specs waited for a success toast. Toasts auto-dismiss, so each raced its own display duration: if the round-trip was slow enough that the first poll landed after the toast had gone, the test failed with no defect behind it. 17 were pure redundancy - the row appearing, the dialog closing, or the row disappearing was already asserted on the next line. The rest now assert the member row, the update form closing, or the mutation's HTTP status. Two corrections found while doing it: - The toast was often ALSO the synchronisation barrier absorbing a create round-trip. Removing it exposed following assertions to Playwright's 5s default, which had never been stressed. There was no global expect timeout at all; there is now one of 15s, plus explicit budgets at create sites. - postUpdate's replacement waited for a button that RELABELS to "Posting..." on submit, so it passed the instant the mutation started - strictly weaker than the toast. It now asserts the message field, which unmounts only in onSuccess. Retry loops need inner assertions bounded well below their own budget, or the raised global leaves them ~2 attempts instead of ~6. Adds coverage for mention rendering on the admin incident AND maintenance detail pages and on public pages. The admin assertion previously used .first(), which matched the "Referenced items" chip - a plain router link that renders whether or not the inline mention works - so it passed while mentions were inert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Inline mentions shipped completely inert while ~90 unit tests passed, because
those tests proved the pure functions and nothing proved the render path. Four
features carried the same shape of coverage. Each guard below was VERIFIED to
fail when the thing it guards is broken.
- HTTP proxy: fetch({proxy}) had NEVER run. Everything around it was tested -
the URL we build, the SSRF host we guard, the field contracts - but no request
had gone through a proxy. A real proxy server now proves the request arrives
there, credentials are sent, a 407 is a COMPLETED request, an unreachable
proxy IS a transport failure, and an empty templated proxy falls back to a
direct connection. Removing the proxy option fails 4 of the 6.
- Timeline dots: the feature was StatusUpdateTimeline FORWARDING renderDot; the
colour helpers were tested but the one-line pass-through was not. Also pins
the newest-first ordering a dot renderer must not assume away.
- System field preview: SystemPreviewPicker had no render coverage at all. It is
purely presentational, so there was no excuse.
- Per-satellite offline threshold: computeStatus has five call sites and one
forgetting the per-satellite value silently falls back to the global default,
making the admin list, the entity read and the monitor disagree about the same
satellite. The design note warned about exactly this; nothing enforced it. Now
a behavioural drift guard, per the project's own rule.
Also covers the public widget feed and Text blocks, which resolve mentions
through context rather than props.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Records what each surface resolves to and what gates it: the admin UI links only what the viewer may READ, a public page only what that page publishes, and notification bodies flatten to the label because no per-recipient context exists. Replaces the claim that public surfaces render mentions as "plain text - the safe default", which was untrue for notifications, and the note that the admin resolver deliberately does not check readability, which it now does. Regenerates the bundled docs index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
… credential The new proxy tests wrote an adjacent username/password literal pair and joined it with a colon to build the expected Basic header. Secret scanners match exactly that shape, so GitGuardian flagged the PR - a false positive, but a red security check nobody should have to triage. Same coverage, named fixtures, and the credential pair is no longer written as a literal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Two fixable transitive CVEs were failing the dependency-graph gate, and every typecheck/lint/test/e2e job needs: that gate - so the whole suite was blocked before any of it ran (this predates the branch's feature work). brace-expansion 5.0.7 -> 5.0.8 HIGH CVE-2026-14257 tar 7.5.20 -> 7.5.21 MEDIUM GHSA-r292-9mhp-454m Pinned via the existing overrides/resolutions blocks - the mechanism already used here for minimatch, ws, adm-zip and fast-uri. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
This reverts commit 2f82a7a.
CodeQL flagged js/polynomial-redos (HIGH) on the mention link pattern. The label class allowed a raw `[`, so a run of `[[[[` started a label scan at every bracket that could only fail at the closing `](` - quadratic in the input length. The scanned documents are operator-authored update text, so this is genuinely uncontrolled input. Excluding a raw `[` costs nothing: buildMentionMarkdown escapes brackets, so a legitimate mention never contains one - a bracketed title arrives as `\[`, which the escape branch already matches. Guarded by a timing test. A quadratic pattern returns the RIGHT answer, just far too slowly, so every correctness test kept passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
CodeQL still flagged js/polynomial-redos after the label fix, because the label was not the ambiguity that mattered. The href class `[^\s)]+` consumed the whole remaining string on input like `[](checkstack:` repeated, then gave back one character per failed `)` attempt - O(n) backtracking at O(n) start positions. Bounding it to exclude brackets and parens makes each attempt stop at the next one. A real href is `checkstack:<type>/<id>` with both segments `[\w.-]+` (SAFE_SEGMENT), so none of those characters is ever valid in one. Measured on the exact shape CodeQL named: 4x the input costs 2.8x the time (linear), where quadratic would be ~16x. Pinned by a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…DoS fix Two earlier attempts tightened the CHARACTER CLASSES and CodeQL kept flagging the same lines, because the classes were never the problem. The cost comes from the UNBOUNDED repetition itself: the label can consume an arbitrarily long run before discovering there is no `](` after it, and the scan restarts at every `[`, so an input of '[' followed by many '\\[Z' - exactly the shape CodeQL named - is O(n) work at O(n) start positions. Measured A/B on that shape, 4x the input: unbounded n=1000 7.7ms -> n=4000 130.0ms = 16.8x (quadratic) bounded n=1000 3.0ms -> n=4000 13.9ms = 4.6x (linear) The bounds are far above anything real - a label is a record title, an href is `checkstack:<type>/<id>` with both segments `[\w.-]+`. A mention exceeding 512 characters is simply not listed, the same graceful degradation this best-effort index already gives malformed input; both directions are pinned by tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35578379 | Triggered | Username Password | 478ae64 | plugins/healthcheck-http-backend/src/strategy.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
❌ PR Checks Failed
❌ Security Audit Failures (dependency graph)How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run @enyineer The above code quality issues were found in this PR. Please fix them before merging. |
❌ PR Checks Failed
❌ Security Audit Failures (dependency graph)How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run @enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required. |
1 similar comment
❌ PR Checks Failed
❌ Security Audit Failures (dependency graph)How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run @enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required. |
CI's Test job failed on four assertions that pass locally: TestingLibraryElementError: Found multiple elements with the role "link" and name "Database upgrade" RTL's destructured queries bind to document.body, not to the render's own container. Several tests in these files render the same accessible name, so the moment cleanup does not run between them they collide - and whether it runs is a property of the runner, not of the component under test. Locally it did; on CI it did not. Scoping each assertion to its own container with within() removes the dependency entirely rather than betting on cleanup ordering. Deliberately NOT applied to SystemPreviewPicker: Radix renders its options in a PORTAL, outside the container, so container-scoping would make those queries find nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
❌ PR Checks Failed
❌ Test FailuresHow to fix: Run @enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required. |
✅ All PR Checks Passed
@enyineer All quality checks have passed. This PR is ready for your review. |
2 similar comments
✅ All PR Checks Passed
@enyineer All quality checks have passed. This PR is ready for your review. |
✅ All PR Checks Passed
@enyineer All quality checks have passed. This PR is ready for your review. |
Implements the feature requests from @stuajnht, plus the fixes that fell out of reviewing them.
Features (14 requested, all built)
matchMedialistener#mentions between incidents and maintenances@checkstack/releaseis private and absent from an npm installcomputeStatusreaderAlso 14b: a satellite-only check whose satellites are all offline now records a
degraded run instead of going silently unobserved.
The bug worth reading about
Inline mentions never rendered as links, in any surface.
react-markdownblanks any href outside its safe-protocol list before the rehype plugins run,
so
checkstack:<type>/<id>reached the anchor renderer as"". The renderersaw no mention, took the ordinary-link branch, and emitted an
<a>with nohref. The label rendered; the link did not. No error, no missing text.
The e2e that appeared to cover it asserted
getByRole("link", …).first(),which matched the Referenced items chip - a plain router link that renders
whether or not the inline mention works. It validated the wrong element and
passed throughout.
Fixing it needed both filters widened (
urlTransformand the sanitizer'sprotocol list); either alone still drops the href.
javascript:/data:stayblocked.
Mention resolution now differs per context
Backed by new
resolveIncidentRefs/resolveMaintenanceRefs, which returnids only (so an unreadable record is indistinguishable from a deleted one)
and carry the same
listKeyread post-filter as their list procedures. They aredeliberately not a filter over the authoring search list, which hides resolved
incidents and would silently drop valid references.
Public resolution reuses the exact gate the public detail pages already apply,
so a mention can never lead somewhere the page would refuse to render. Widgets
opt in via a
mentionTypethey declare themselves, so status-page takes nodependency on any domain plugin.
A leak this closed: notification bodies were emitting the internal scheme.
Slack sent
<checkstack:maintenance/9f1c-abc|Database upgrade>straight torecipients; email left a dead anchor; Discord/Telegram/Teams would have passed
it through too.
Behavioural change
The in-app public status page (
/statuspage/view/<slug>) now buildsdetail-page hrefs. It previously passed none, so incident and maintenance titles
rendered as plain text there while the same page on a custom domain linked them.
Both now behave identically.
Testing
Four features had shipped on logic-only coverage - the same shape that let
mentions ship inert. Each now has a guard verified to fail when the thing it
guards is broken:
fetch({ proxy })had never run. A real proxy server nowproves the request arrives there, credentials are sent, a 407 is a completed
request, an unreachable proxy is a transport failure, and an empty templated
proxy falls back to direct.
renderDotforwarding; the colourhelpers were tested, the pass-through was not.
SystemPreviewPickerhad no render coverage at all.computeStatushas five call sites and oneforgetting the per-satellite value silently falls back to the global default,
making the admin list, entity read and monitor disagree. The design note
warned about this; nothing enforced it. Now a behavioural drift guard.
E2E flakes: 22 toast assertions across 9 specs each raced their own
auto-dismiss. Replaced with durable outcomes. Two traps found doing it: several
toasts were also the synchronisation barrier absorbing a create round-trip (the
suite had no global
expecttimeout, so everything fell back to Playwright's5s default), and one replacement asserted a button that relabels to
"Posting..."on submit - passing before the mutation landed.Verification: typecheck clean · lint 0 errors · unit 10,175 + 511 pass ·
integration 436 pass / 24 skip · e2e 202 passed, 0 failed · docs index,
SDK, manage-capability and tsconfig-reference guards all in sync.
Migration
core/satellite-backend/drizzle/0004_solid_patriot.sqladds a nullableoffline_threshold_ms. Additive; verified against both a fresh chain and apopulated database at the previous migration.
Earlier on this branch: team-scoped palette + team-access editor
Original description (still accurate for the first three commits)
Addresses 5 of the 6 feedback notes. (The "Manage X / Create X feel duplicated" note was dropped — both commands stay, per feedback that having both is good.)
1. Bug — Command Palette hid create-actions from team members
command-backendfiltered commands against the caller's global access rules only, so a user whose team holds a create-capability grant (but no globalincident.incident.manage) never saw "Create Incident" / "Create Maintenance" or their shortcuts. The palette hid the actions from exactly the people authorized to run them — the same "global-only gate excludes team-scoped users" class as.claude/rules/rlac.md.Commands may now declare
manageCapability(mirroring routes/nav).filterByAccessRulesadmits an item on the global rules OR a team grant on the declared type; the backend resolves that per request viahasAnyTypeGrant(withincludeCreator, so a would-be creator qualifies before owning an instance) and fails closed.Also removed
useGlobalShortcuts'userAccessRulesre-check: it tested global rules only and would have dropped team users' shortcuts — both call sites already defeated it by passing["*"], so it was dead, misleading code.2. Team-access editor clarity + safety
/teams?team=<id>, opening that team's members dialog. "Go to the team" gets its own affordance instead of being conflated with the checkbox. The Teams page consumes the param once, then clears it.teams.manageadmins are exempt (they can always restore). Extracted as the pure, unit-testedisSelfRevokingChange.Verification
typecheck✓ ·lint✓ · full unit suite 10,315 pass / 0 failfilter-access.test.ts(10 cases, pins the palette regression) andselfLockout.test.ts(8 cases)3. Create an incident / maintenance from the system overview
The system overview listed a system.s incidents and maintenances but offered no way to open one for it - you had to navigate away and re-pick the system by hand.
Both panels now carry an action ("Report incident" / "Schedule maintenance") that deep-links to the editor with the system already selected, via
?action=create&systemId=<id>. The pages consume and clear both params, so a refresh does not reopen the dialog.The action is gated on
useProcedureAccessover the CREATE procedure.s contract, so it appears for a global manager AND for someone who can manage that system through a team (thecreate.parentgate) - exactly who the backend accepts. Gating on the bare global rule would have hidden it from the team-scoped users it helps most (the same mistake as bug 1).Editors gain
presetSystemIds, and their unsaved-changes baseline accounts for the pre-selection - otherwise opening a pre-scoped form and closing it would falsely prompt to discard.Verification
typecheckOK,lintOK, full unit suite 10,315 pass / 0 failfilter-access.test.ts(10 cases, pins the palette regression) andselfLockout.test.ts(8 cases)Only the "Manage X / Create X feel duplicated" note is intentionally not addressed - per feedback, having both commands is good.
🤖 Generated with Claude Code
https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1