mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-10 17:36:04 +00:00
feat/gitops-poll-scheduling
1961 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ee7ad463a |
test(gitops): pin the source-row rollback in the 409 race test
The 409 test asserted only the application row; the source row is written by the same transaction, so its survival is what proves the save rolled back whole. |
||
|
|
ab0c5457fe |
style(gitops): re-indent the upsert transaction body under its new try wrapper
Whitespace only: the save transaction gained a try/catch in
|
||
|
|
53e97e5697 |
fix(gitops): consume the retry cursor too when a source moves to manual
The poll-cursor clear missed the retry cursor: an automatic source in a backoff window that was flipped to manual would stay in the retry-due set forever, since the retry scan has no policy filter and the controller's manual guard declines without consuming. Moving to manual now withdraws both schedules while leaving failure evidence untouched, and the rescheduleAll docstring matches the guard the previous commit widened. |
||
|
|
3b10206b0d |
test(gitops): drive the backoff deferral test through the real due queries
The test replaced both due queries with JS filters that mirrored the intended SQL predicate, so a regression in the query itself would pass it; the real SQL was only covered by the separate eligibility suite. Eligibility is the behavior under test here, so the real store queries now run against the seeded row and the deferral claim holds end to end. |
||
|
|
97ca5ee94a |
fix(gitops): skip reschedule for any unconsumed retry cursor
The guard only skipped a future retry_at, so a fired-but-unconsumed cursor still got a poll cursor armed that the poll scan would never honor: the exact inert-cursor audit line and misreported next wake the skip exists to prevent. Aligning the guard with the poll scan's any-cursor exclusion closes the mismatch; the retry scan still owns the row either way. |
||
|
|
33b43fc704 |
fix(gitops): consume the poll cursor when a source moves to manual
The policy-flip path made manual-with-armed-cursor reachable for the first time: the poll scan picked the row every tick, the controller's manual guard declined it without consuming the cursor, and the projection kept reporting a scheduled poll that could never run. Moving to manual now clears next_poll_at, matching how fetchStarted consumes the cursor; non-manual policies keep it. |
||
|
|
dcc645c268 |
fix(gitops): map an in-flight refusal during source save to 409
A PUT that changes the source policy while a fetch is in flight threw GitOpsTransitionError out of the upsert transaction. The rollback was correct, but the route mapped the throw to a generic 500, hiding the actionable reason. Suspend and dismiss already translate this error to an OPERATION_IN_FLIGHT GitSourceError; the save path now does the same, and a route test pins the 409 and the full rollback. |
||
|
|
71bdf744e5 |
test(gitops): unit-test sourcePolicyChanged persistence and in-flight guard
The route suite covers the upsert path end to end, but the transition itself had no direct tests: one pins that the policy row and history land, the other pins that a fetch in flight refuses the edit, which is the conflict the settle path depends on. |
||
|
|
e9fb138f35 |
test(gitops): pin suspended-source policy edits, audit row, and index rebuild
Three coverage gaps from the test-analysis pass: a policy PUT on a suspended source must succeed (suspension gates work, not configuration, and a future hardening refactor that re-adds the guard would 500 operator edits); the source_policy_changed history row is asserted so a dropped audit insert fails loudly; and a PATCH with a backoff row exercises the rescheduleAll skip through the HTTP route. The poll due index is dropped before creation because CREATE INDEX IF NOT EXISTS never updates an existing index, so upgraded installs would keep the pre-retry_at WHERE clause; the schema SQL now forces the rebuild. |
||
|
|
8708b15413 |
docs(gitops): correct comment claims about retry-deferral and policy persistence
The rescheduleAll skip comment attributed mid-backoff refetch prevention to the wrong guard (the poll-scan SQL exclusion does that); it now states what the skip itself delivers: no inert poll cursor, no misreported next wake in the polling projection, no spurious audit line. The retry-query test comment describes the defensive property it actually pins instead of an unreachable origin story, the listSourcesDueForPoll doc names the retry-cursor exclusion, the sourcePolicyChanged doc drops the liveness claim the method does not enforce (callers resolve the live row), the duplicated create-path narrative is kept in the method doc alone, and a temporal marker is removed from a test comment. |
||
|
|
2d8ccafdd6 |
refactor(gitops): allow policy edits on a suspended source
Suspension gates unattended fetching and applying, not configuration: the operator must be able to re-policy a suspended source before resuming it. The in-flight guard stays, since the settle path reads the policy when deciding acceptance. |
||
|
|
0e889e0084 |
fix(gitops): persist source policy when upserting an existing source
PUT accepted source_policy and the legacy boolean but persisted them only through the create path, so an operator editing an already-linked source could not leave or return to the automatic policy; the field resolved and then vanished. The existing-app branch of the upsert transaction now records a source_policy_changed transition when the resolved policy differs from the stored one. A boolean 0 or omitted still resolves to the existing policy, so the no-silent-conversion rule is unchanged. |
||
|
|
8ea2959be2 |
fix(gitops): poll eligibility defers to a future retry cursor
A source inside a fetch backoff window kept its poll cursor visible to the due scan, so a cadence change or an unlucky tick could refetch the remote before the backoff expired. The poll scan now excludes rows with a retry cursor, the matching partial index mirrors the term, and rescheduleAll leaves backoff rows alone so the retry cursor stays the next wake. Retry-due rows still arrive through the retry scan and are deduplicated per application. |
||
|
|
fbaab63946 | test(gitops): drop unused reconcile handle in polling suite | ||
|
|
fa35d12820 | perf(gitops): partial indexes for poll and retry due queries | ||
|
|
583b577bd9 | feat(git-sources): node-scoped polling settings endpoint | ||
|
|
466f7d5ca4 | feat(gitops): controller retry scheduling and automatic acceptance | ||
|
|
2d212d255f | feat(gitops): poll scheduling transition and fetch failure evidence | ||
|
|
9ef3580852 | feat(git-sources): source policy compatibility for legacy boolean clients | ||
|
|
01079076eb | feat(gitops): migrate legacy auto-apply boolean to source policy | ||
|
|
72fd455346 | fix(deps): bump grpc-go to v1.83.2 | ||
|
|
69e76090cc |
feat(gitops): durable reconcile-attempt reservation, coalescing, and recovery (#1893)
* feat(gitops): add reconcile trigger normalization and coalescing keys The source controller needs a single normalized shape for every trigger that can request evaluation (manual, API, webhook, poll, retry, config change, startup, resume, and future provider/schedule/binding-change producers), and a way to decide whether two concurrent submissions describe the same work. Add ReconcileTrigger, the discriminated ReconcileRequest (fetch vs apply), and coalesceKey/deliveryKey. A fetch coalesces by application alone, since it has one live outcome regardless of trigger; an apply coalesces only when the commit, plan fingerprint, and deploy choice all match, so two applies that differ in any of those can never be joined and have one silently receive the other's result. * feat(gitops): add exhaustive failure classification and retry backoff The controller needs to tell a transient network condition from a permanent configuration one from a broad range of Git-source, policy, and target failures, since several distinct causes collapse onto the same public error code and a wrong call either retries forever on a bad URL or gives up on a DNS blip. Add classifyFailure, built on two total lookup records (one keyed by TransportFailureReason, one by GitSourceErrorCode) so adding a new value to either source union fails the build until this classifier accounts for it, rather than silently defaulting. A tip-changed race classifies as supersession, not backoff; an unrecognized exit-coded git error gets a lower retry ceiling than a plain network timeout; a deploy or health failure after a successful apply is its own class that must never refetch or reapply. Add nextRetryAt: bounded exponential backoff (60s doubling, capped at one hour) with +-10% jitter, honoring a provider-supplied retry floor when it is larger than the computed delay. * feat(gitops): derive normalized reconcile outcomes from the source facet Silence is not an acceptable GitOps result: every reconcile attempt needs to settle into one named outcome an operator can act on, not a bare success/failure. Deriving that outcome from the existing source-facet projection (rather than a second, parallel status source) is what keeps "no source change" from silently collapsing into "converged" the way a bare commit-SHA comparison would. Add outcomeFromSourceFacet, exhaustive over all 17 SourceFacet statuses, each mapped to a ReconcileOutcome, a human reason, and a next action (review, resume, retry, resolve_conflict, configure_credentials, view_target_results, or none). converged is deliberately never produced here: it requires target and health evidence this source-only projection does not carry, so a later composition over source + target + health is the only thing allowed to report it. * feat(gitops): add the portable accepted-generation contract Direct and Blueprint dispatch need to consume the exact same description of what an accepted generation contains, without either side inventing a node id, local candidate path, target project name, or placement field into it, since that is exactly the kind of drift that would let a stale acceptance authorize a routing decision made after it. Add six additive, nullable columns to gitops_generations (portable manifest, Compose inputs, source/security policy evidence, support and compatibility requirements), all decoded honestly: a legacy row missing one records an explicit limitation rather than inventing evidence, and a legacy pending candidate lacking the new contract must be re-evaluated before it can be accepted or dispatched. Add gitops/handoff.ts: the AcceptedGeneration type built from a generation row via buildAcceptedGeneration, a compile-time assertion that the contract cannot carry a target-mode-specific field, and the TargetAdapter boundary with BlueprintTargetAdapter failing closed until Blueprint rollout orchestration exists. Current target mode and binding travel separately in DispatchContext, re-read under the dispatch lock rather than carried on the generation itself. * feat(gitops): add controller-owned bookkeeping columns to gitops_applications The source controller needs somewhere to persist source policy, poll cadence, and a durable attempt sequence per application, and it needs to work identically for Direct and Blueprint applications. stack_git_sources (the existing home for auto_apply_on_webhook/auto_deploy_on_apply) is Direct-only and keyed by stack name, so it cannot represent a Blueprint application at all. Add source_policy (manual|review|automatic, default manual), poll_interval_secs (NULL inherits the global default, 0 disables), next_poll_at (the durable scheduling cursor), and attempt_seq to gitops_applications instead. All default to values that start no unattended work: an upgraded installation begins polling nothing and stays on manual policy until explicitly migrated or configured. * feat(gitops): add durable attempt reservation and poll/retry queries History dedupe alone is not execution idempotency: mutateApp writes the application row and only then inserts history, so a dedupe conflict still commits the row write. The controller needs a reservation that runs before any side effect and touches nothing else, so a duplicate or restarted submission can be told apart from new work without repeating it. Add reserveReconcileAttempt/settleReconcileAttempt: a bare history insert in its own transaction, deliberately not through mutateApp, using the existing history dedupe index as the reservation/idempotency check itself. A repeated reservation or settlement for the same operation is a no-op, never overwriting the first settled result. Add the store queries a controller needs to drive this: getSettledAttempt and latestSettledAttempt (exact vs. most-recent lookup, the latter tie-broken by rowid since the id column is a random UUID unrelated to recency), listUnsettledReconcileAttempts (a reservation with no matching settled row, for startup recovery), and listSourcesDueForPoll / listApplicationsDueForRetry (excluding suspended, in-flight, and Blueprint-mode applications). * feat(gitops): add controller-facing reconcile entry point to GitSourceService Adds GitSourceService.reconcile(), a single normalized entry point that takes a fetch or apply request and returns a normalized outcome, next action, and reason instead of a thrown error or a raw boolean. It owns the git mutex for the whole evaluation and calls the same private fetch/apply bodies the existing pull()/apply() routes use, so it never nests locks. Resolves the live application for the stack first and fails closed, without attempting any work, when the request's application id no longer matches it or when no application exists at all. A fetch or apply failure that the underlying transition never persisted (missing config, a stale commit, lock contention) is classified through the existing retry disposition table so an unretryable failure is never reported as retryable, and a stale-looking success is never reported in place of a real failure. * feat(gitops): dispatch accepted generations to their target Adds GitSourceService.dispatchAcceptedGeneration(), which routes a portable accepted-generation contract to its target: Blueprint mode delegates to the existing BlueprintTargetAdapter (rollout orchestration is not built yet, so it always blocks), and Direct mode dispatches by driving reconcile() with the generation's commit, since there is no separate generation-based promotion pipeline yet. The deploy flag is read from the stack's own auto_deploy_on_apply setting, matching how every other producer decides it. Also fixes a case reconcile() got wrong: when a promotion succeeds but the following deploy fails, the source itself has genuinely changed, so reporting it as an unchanged failure was as untruthful as reporting it a plain success would have been. It now reports a result that neither claims, pointing at the target instead of asking for a source retry that could never succeed. * feat(gitops): add background poll and retry driver for source reconciliation Adds SourceController, a self-rescheduling background timer that finds sources whose poll interval or retry time has arrived and evaluates each through the existing reconcile() entry point. A per-application in-flight set is what keeps one slow evaluation from blocking the rest of the fleet; the tick never waits on any evaluation before scheduling the next one, and a scan that throws still reschedules rather than stopping the driver permanently. This delivery only covers detection: a tick issues a fetch, the same step a manual pull performs, so a new candidate is staged for review but not automatically accepted or dispatched, and a retry re-issues a plain fetch rather than resuming whatever stage previously failed. Both are documented as open follow-on work rather than silent gaps. Not yet wired into the process lifecycle; that follows separately. * feat(gitops): start and stop the source reconciliation driver with the process Wires SourceController into the background-service lifecycle: it starts after the existing GitOps recovery and orphan-sweep steps, so it never scans an application still carrying a stale in-progress marker from a killed process, and stops alongside every other background timer on shutdown. * feat(gitops): add suspend, resume, and explicit retry for a source Adds GitSourceService.suspend()/resume()/retry(), the service-layer methods behind an upcoming suspend/resume/retry control surface. Suspending a source now genuinely stops it: pullLocked and applyLockedBody both check suspension before doing any work, not just inside the transition bookkeeping, which used to reject but then let the fetch or apply proceed anyway. A refused suspend surfaces as a real error rather than a silent no-op, since an operator believing a source is suspended when it isn't is the exact failure this exists to prevent; a refused resume stays silent, since the row read back after the attempt already reports the true state either way. A webhook delivery to a suspended source is reported as skipped rather than a failed pull, so a long suspension does not read to the Git host as a broken webhook. * feat(gitops): add suspend, resume, and retry routes for a git source Adds POST endpoints for suspending a source (with an optional, length- capped reason), resuming it, and explicitly retrying it, wired to the existing service-layer methods behind the same stack:edit permission as pull and apply. Neither route can trigger a deploy today, so unlike apply and webhook-pull they need no conditional stack:deploy check. * fix(gitops): close path-injection gaps at two candidate-file sinks Adds the inline resolve-and-prefix-check barrier this codebase already uses at other filesystem sinks derived from a stack name or a stored candidate path, closing two sinks that lacked it: the manifest write in GitProjectManifestService, and the pending-candidate access check before promoting an apply. The apply-side check now shares the same strict candidate-path validator the registry-delivery path already uses on the identical stored field, rather than a looser check, so both call sites treat a tampered candidate reference the same way. Fixed two test fixtures that had never matched the shape a real candidate path takes, which the stricter check would otherwise have rejected. * fix(gitops): confine the manifest write directory to the managed area The prior fix checked the manifest write/rename targets against the resolved managed directory, but left the mkdir call on that directory itself unconfined and checked it against the data root rather than the narrower managed area every sibling barrier in this file uses. Aligns it with the established convention and the actual boundary that matters. * feat(gitops): wire durable attempt reservation and coalescing into reconcile() reconcile() now reserves a durable attempt before doing any fetch or apply and settles it with the normalized result once execution finishes, using the reservation and settlement primitives that already existed but had no caller. A request carrying a stable external delivery id reserves under a producer- and intent-namespaced key, so a redelivery reuses the same attempt instead of minting a second one; a request with no such identity gets a freshly allocated attemptSeq- based id, bumped atomically with its reservation. Concurrent submissions that would do the same work now coalesce: the first becomes the leader and actually runs, and any submission that joins while it is still in flight awaits the leader's real result instead of running a duplicate fetch or apply. Each still gets its own durable attempt and its own settled row, including a concurrent redelivery of the same external event, which joins the in-flight leader rather than falling back to a snapshot of the row from before the leader's work landed. Startup gains an attempt-recovery phase, run before the managed-area sweep and before SourceController starts: every attempt reserved but never settled, most likely from a crash between the two, is resolved from durable state without re-executing anything. One row failing to recover no longer blocks the rest; it is skipped and logged, and recovery keeps paging until nothing unsettled remains. A settlement failure is caught and logged rather than turning an already-successful fetch or apply into a thrown error for the caller, and a settled attempt's stored result is now decoded through a validated outcome/next-action check instead of a blind cast, logging rather than silently reporting unknown when a stored row is corrupt or unreadable. * fix(gitops): remove unused store variable from coalescing test * fix(gitops): make reconcile-attempt recovery leader-aware and cursor-paginated A fresh audit found real gaps in the reservation/coalescing wiring from the previous commit: recovery paged by "still unsettled" status rather than a cursor, so once a permanently unrecoverable row occupied every slot in a page, every genuinely recoverable row beyond it was silently never reached; a follower's outcome was reconstructed independently from row state rather than from its leader's actual stored result, so a leader and its follower could durably disagree; and an attempt resolved on the live path was returned but never actually settled, leaving it open indefinitely until the next restart. listUnsettledReconcileAttempts now takes an optional (created_at, id) cursor, matching the pagination shape queryHistoryRows already uses, so paging always advances regardless of which rows settle. Recovery is now two-pass: independent attempts settle first from row state, followers are deferred, then each deferred follower settles from its leader's now-settled result. A follower whose leader is a real but still-unresolved reservation is left unsettled rather than guessed at independently, since the leader could still settle to something else later, including when the leader simply failed to settle in this same pass rather than "never will". The same leader-aware resolution now backs the live reconcile() path too, via a shared helper, so an already-reserved attempt is durably settled instead of merely returning a value. Also fixes a narrower race in reconcile() itself: two submissions can share an operation id (a stable external delivery id) while running under different coalesce keys, since an apply's coalesce key includes its commit sha, plan fingerprint, and deploy flag, which the delivery id does not carry. reconcile() now checks in-process executions by operation id directly before falling back to a durable-state resolution, so such a submission joins the real in-flight leader instead of settling a stale pre-execution snapshot ahead of it. * feat(gitops): route pull, apply, and webhook producers through reservation A fresh audit found the same "primitive built, real caller does not use it" pattern one layer deeper than the previous commit fixed: reconcile(), the controller-facing entry point, reserved and coalesced durable attempts correctly, but the actual production producers, the manual pull button, the manual apply button, and the webhook route, all called pullLocked/applyWithSharedLock directly, bypassing reservation entirely. pullLocked also minted its own independent operation id rather than using a reserved attempt's, breaking the "one operation id spans an attempt and its stage evidence" invariant. pullLocked, applyWithSharedLock, applyLocked, and applyLockedBody now accept an optional operation id, using it in place of their own default when a caller supplies one. pull() and apply() reserve a durable attempt and coalesce with a concurrent call to themselves whenever a real GitOps application exists for the stack, the same definition pullLocked itself already used to decide whether it has any GitOps bookkeeping to do at all. handleWebhookPull()'s fetch step and its conditional auto-apply step reserve too, without a coalescing map: the route's own debounce window plus its single per-stack lock acquisition already prevent a concurrent duplicate from reaching that point, so there is nothing to coalesce there. Fixed along the way: apply()'s coalesce key computed its deploy flag differently than the code that actually executed the apply, so two concurrent applies that genuinely differed in deploy behavior could share a key and one could silently receive the other's result; deploy is now resolved once, the same way applyLockedBody itself resolves it, before it drives either the key or the execution. A policy-bypassing apply is now routed through a fresh, never-shared coalescing map, since bypassPolicy changes behavior but was not part of the key. A coalesced follower's own reservation is now settled in a finally rather than only after a successful await, so a rejecting leader (the ordinary path for a producer that preserves its own throw contract, unlike reconcile()'s internal error handling) no longer leaves the follower's attempt open until the next restart. A reservation bookkeeping failure no longer turns a manual pull or apply that would otherwise have succeeded into a hard failure; it logs and falls through to unreserved execution instead. * fix(gitops): make the boot sweep consult claimant pointers before reaping a candidate The boot-time managed-area sweep decided whether to delete a staged candidate directory purely from file age and a completeness marker, with no awareness of the database. A fresh audit gave the concrete failure: a reconcile stages a candidate, the process crashes before settlement, the installation stays down more than a day, startup settles the attempt from a snapshot rather than real stage evidence, and the sweep then deletes the still-needed candidate out from under it. sweepManagedArea now takes the set of candidate directory basenames still referenced by the stack, and never reaps one of them regardless of age or completeness. GitSourceService computes that set from three independent sources: the live application's current candidate generation, its accepted-but-not-yet-promoted generation (the sourceAccepted-committed, targetApplied-not-yet-committed window; that path has no production caller yet, so this is forward-looking coverage for it), and the pending fetch record's own candidate reference, which is written outside the transaction that mints a generation and can therefore be the only claimant for a candidate that failed validation or was staged while no live application existed to read a pointer from. * fix(gitops): fail closed on reservation failure and a torn-down application A fresh audit found that a reservation-bookkeeping failure (a transient DB error, an application torn down in the window between resolving it and reserving against it) fell through to unreserved execution. That directly defeated this delivery's own purpose: a manual apply could still promote Compose files and deploy with zero durable record of it happening. Reservation failure now fails closed for pull, apply, and the webhook route: the operation is refused, logged, and recorded to the stack's own activity history, distinguishing a torn-down application (never retryable) from a transient failure (worth retrying). Closing that hole surfaced a second, related gap: pull, apply, and the webhook route only checked for a live (active) application before deciding whether to reserve at all, so a stack whose GitOps tracking was explicitly torn down while its Git source configuration survived could still run fully untracked, the same failure mode reached a different way. Refusing this case took two attempts to get right, both caught by review before landing: the first version refused on any tombstoned state, which would have permanently and unrecoverably blocked pull and apply for an application deliberately tombstoned as deleted while its config survives for a future rebuild, a state two existing production paths produce on purpose; narrowing to detached only still misfired on a routine, fully completed detach, since that same operation deletes the source row in the same transaction, so the refusal must also confirm the source row actually survived before firing. Both are covered by regression tests now, alongside the original reservation-failure fix. * fix(gitops): unify fetch-intent coalescing across pull and reconcile A manual pull and a concurrently poll-triggered reconcile for the same application previously ran their own separate in-flight maps and could each start a clone for the same fetch. They now share one coalescing map, with reconcile's fetch path classifying its own outcome instead of relying on generic row-state derivation, so a pre-transition failure the row does not yet reflect is never durably recorded as a plain success. Apply-intent coalescing stays producer-local for now; unifying it needs deploy-failure awareness threaded into the shared settlement path first, which is a separately scoped follow-up. * feat(gitops): recognize a webhook delivery id for traceability The real webhook trigger endpoint is generic and HMAC-signed, with no delivery identity of its own. It now extracts one from a recognized provider header (GitHub, GitLab, Bitbucket, or a generic fallback) when the caller sends one, and threads it through to the git-pull execution path as a plain traceability breadcrumb on failure logs. Redelivery dedup is deliberately not implemented here: an earlier attempt routed the delivery id through the durable attempt reservation itself, which silently dropped a redelivery's history through the existing dedupe index instead of recording it. Building real dedup needs settlement to reflect classified outcomes rather than generic row-state derivation for both a settled and a crashed-mid-flight prior attempt, which is a wider, separately scoped change shared with the same gap already deferred for apply-intent reconcile. * fix(gitops): thread the reserved attempt's operation id into the pending fetch record The pending fetch record stamped its own independent random operation id instead of the reserved attempt's real one, so an apply falling back to it (when it holds no reservation of its own) inherited an identity unrelated to the fetch that actually produced the candidate. One id now spans reservation, fetch, generation, and the pending record. Also fixes the short "op" token rendered in activity log lines: a fixed prefix stopped discriminating between attempts once operation ids became structured (<applicationId>:attempt:<seq>), since the prefix is now the same applicationId every time. A shared helper renders the actual attempt-discriminating suffix instead, applied consistently across pull, apply, and create so each event's logged identity matches what its own durable history actually recorded. * test(gitops): cover the reconcile-recovery-then-sweep startup ordering Reconcile-attempt recovery and the managed-area sweep must run in that fixed order before the source controller's own poll loop starts, but the guarantee lived only in a comment inside startServer, a function with roughly two dozen unrelated service initializations that makes it impractical to exercise end to end in a test. Extracted the two steps into their own function so they're directly testable in isolation, without moving the source controller's own start call: that stays exactly where it was, since pulling it earlier would have crossed a separate, already-documented ordering requirement for registry delivery recovery. A structural test guards the one property that can't be covered by driving the function directly: that the real startServer body still calls the extracted function before starting the controller. * fix(gitops): complete durable reconciliation execution * fix(gitops): resolve static analysis findings |
||
|
|
1ad20da31f |
Surface runtime build identity across About, shell, and Admiral Account (#1899)
* feat: surface runtime build identity across About, shell, and Admiral Account Add one canonical source for the control instance's runtime build identity so a development image is visibly identified even when its packaged semver still matches the previous stable release. SelfIdentityService now retains the running image reference and assembles a BuildInfo (version, channel, imageRef, imageId, revision) via a bounded, failure-isolated image-inspect step during initialization. getBuildInfo() is a cached read that never triggers Docker. classifyBuildChannel() labels the running reference stable, dev, preview, or unknown. A new proxy-exempt GET /api/build-info returns that identity to a signed-in human session, redacting hardened image references to non-admins with a restricted flag so the UI shows Restricted rather than Unknown. Public /api/meta gains a bounded buildChannel enum without leaking the image reference. A BuildInfoProvider context shares one fetch across the About section, the sidebar DEV/PREVIEW chip, the mobile tab bar, and the Admiral Account Channel and Current image rows, keeping the control-instance identity separate from remote nodes. * fix: move chipDetail helper out of the component file to satisfy fast refresh SidebarBrand.tsx exported both the SidebarBrand component and the chipDetail helper, which trips react-refresh/only-export-components and fails the lint gate. The helper now lives in its own chipDetail.ts module so the component file exports only the component; the direct unit test keeps its coverage by importing from the new module. * fix(build-info): resolve revision-enrichment race and harden identity rendering - Await the detached revision enrichment before serving /api/build-info so a successful response never freezes a transient null revision. - Lay the mobile DEV/PREVIEW pill in-flow as a non-overlapping flex sibling instead of an absolutely positioned overlay. - Wrap long image and revision tokens with break-all so they do not overflow the About panel. - Surface a toast when copying the image id fails instead of leaving an unhandled rejection. |
||
|
|
b6766572df |
feat(navigation): make Compact Launcher the default desktop style (#1890)
* feat(navigation): make Compact Launcher the default desktop style Compact Launcher replaces Smart bar as the default desktop navigation style; Classic bar is fully retired (rendering, settings option, and persisted-value migration to Compact). Quick-link capacity increases from 7 to 8, and the recommended default set becomes Home, Fleet, Resources, Security, Update, and Schedules. Quick-link seeding and Reset now use a settled, role-aware eligibility computation distinct from the existing current-context display filtering, so an operator's recommended defaults reflect their role rather than which node happens to be active, and a Reset produces the same result regardless of node context. Also fixes two issues surfaced while touching this code: a disabled quick-link control showing a duplicate tooltip (native title plus the Radix tooltip), and the Navigate launcher panel being unable to scroll at constrained viewport heights (now uses the shared ScrollArea component). The launcher hamburger icon animates into an X on open, respecting Reduced motion. * test(e2e): fix the launcher morph and panel scroll navigation assertions Three assertions in the new navigation specs were wrong against a real browser, all in the tests rather than the product: - The morph check read getComputedStyle().transform, which Tailwind v4 no longer writes: rotate-45 compiles to `rotate:45deg` and the translate utilities to the standalone `translate` property, so both the open and closed reads returned "none". It now snapshots translate and rotate alongside transform. The animation itself was always correct, since Tailwind's transition-transform covers translate, scale, and rotate. - The reduced-motion check assumed motion starts enabled. A fresh install defaults to the Calm visual style, which turns Reduced motion on, so the clamp was already active and the pre-toggle duration assertion could never hold. It now drives the toggle in both directions from a known state. - The panel scroll check asserted a specific overflow measurement, which depends on Radix having applied its available-height variable at read time and on how many destinations the account can reach. It now asserts the property the fix actually guarantees: the ScrollArea viewport owns vertical scrolling while the outer menu only clips, with no horizontal overflow and the panel inside the viewport. Verified by running the spec against live dev servers: 7 passed, twice. * fix(nav): make the Navigate launcher panel actually scroll with the mouse Live QA found that the panel only reached destinations below the fold by keyboard; a real mouse wheel did nothing. The Radix ScrollArea viewport is sized with h-full, and a percentage height only resolves against a containing block with a definite height. The popper content is height:auto clamped by max-height, which is not definite, so nothing sized from it is either, so the viewport fell back to its full content height with no internal overflow, no scrollbar, and no response to wheel input, while an ancestor's overflow-hidden silently clipped everything past the fold. Moves the available-height cap onto the viewport itself and moves the masthead inside the scroll region so the cap needs no masthead-height arithmetic. Verified live: the previous structure measured scrollHeight === clientHeight === 594 with zero wheel movement; the fix measures 646/357 with the wheel reaching the true maximum. Also hardens the panel's e2e coverage, which passed on the broken structure because it asserted only computed overflow properties and never drove a real scroll: adds a keyboard-reach assertion using End rather than ArrowDown (ArrowDown landing on the first item is stock roving focus and proves nothing about scrolling), a genuine mouse-wheel assertion that scrolls to the true bottom rather than assuming one gesture covers the range, and a companion test proving the cap tracks the popper's available height rather than a hardcoded pixel value. Confirmed the rewritten test fails on the previous structure and passes on the fix. |
||
|
|
607904ee63 |
fix(editor): pin compose editor requests to the captured node (#1897)
Saving a compose file failed with 503 when another Sencho tab had switched the active node: apiFetch falls back to the shared sencho-active-node localStorage key on every request, and a second tab rewrites that key, so the first tab's save PUT silently targeted the wrong node. Refreshing the first tab rewrote the key and made the same save succeed, matching the report. The compose/env load chain and the save PUT now carry an explicit node id captured when the operation starts, so no request in a load or save operation can be retargeted mid-flight. LoadFileOptions gains nodeId (number pins that node, null pins the local node, undefined keeps the follow-active behavior), threaded through loadFileCore, the env, backup and effective-services loaders, the containers fetch ownership check, changeEnvFile, and saveFile including the 412 forced retry. Regression tests: hook tests pinning every request in the chain, the 412 retry consistency case, the cross-node load lifecycle with the containers ownership check, and a two-tab e2e that switches the node in a second tab and asserts the first tab's PUT carries its own node id. |
||
|
|
6fc4a43a04 |
fix(hub-only): enforce hub-only /api/api-tokens/ boundary (#1894)
Add /api/api-tokens/ to HUB_ONLY_PREFIXES. Reuses existing case-insensitive isHubOnlyPath matcher. Full API-token tests cover GET/POST/DELETE for both Proxy and Pilot agent modes, plus query-param (Pilot), mixed-case path, and hub-local CRUD lifecycle using the created token's returned ID. Existing SSO matrix, disconnected-Pilot, and mixed-case Secrets regressions are preserved. |
||
|
|
da905ab07c |
refactor(gitops): split source acceptance from Direct target application (#1892)
* refactor(gitops): split source acceptance from Direct target application applied() bound source acceptance and Direct target application in one mutation, so a future dispatch path could not accept a candidate and defer binding a target until after promotion. Extract the mode-neutral and Direct-only mutations into shared helpers, and expose them as sourceAccepted and targetApplied. applied() now composes the same helpers in the same transaction and emits the same single history row, so its observable contract is unchanged. targetApplied refuses to bind a target to a generation the application has not accepted, so a target can never be bound to source content nothing authorized. * fix(gitops): move apply's cache invalidation and post-deploy scan into the service invalidateNodeCaches and triggerPostDeployScan were called by the manual apply route only, so a webhook-driven apply (and any future poll, retry, or resume trigger) never invalidated caches or ran a post-deploy scan. Move both into GitSourceService.apply() itself: cache invalidation fires once whenever promotion commits, apply-only and deploy-failed outcomes included, since the authoritative Compose files have already changed by then regardless of whether a deploy followed; the post-deploy scan still fires only after a successful deploy. Remove the now-redundant calls from the apply route so each still runs exactly once. * feat(gitops): add tri-state candidate policy evaluation The deploy-time policy gate deliberately fails open (ok: true, trivyMissing: true) when the scanner is unavailable, so an operator is never blocked from deploying. That fail-open behavior must not extend to automatic GitOps source acceptance: an unresolvable scanner state accepting a candidate nothing actually proved safe would defeat the policy gate entirely. Add evaluateCandidatePolicy, a thin tri-state (allowed | blocked | unavailable) wrapper around the existing enforcePolicyForImageRefs evaluator. It takes the candidate's own image refs directly rather than reading compose from disk, so a future caller evaluating a staged candidate (before it is promoted to the live stack) can reuse the same policy logic the deploy gate already uses. * feat(gitops): propagate the raw transport failure reason onto GitSourceError The native transport catch block classified a raw TransportFailure into a sanitized (code, message) pair for GitSourceError, discarding the structured reason (e.g. exit, timeout, target-unresolved) once it had been logged. A future GitOps retry/backoff classifier needs more than the public error code to tell a transient network condition from a permanent configuration one, since several distinct reasons collapse onto the same code (both a DNS failure and a connection reset map to NETWORK_TIMEOUT, and several permanent conditions map to GIT_ERROR). Add transportReason to GitSourceError.extras, carrying the original reason through both throw sites. * fix(gitops): give source suspension its own reason field sourceSuspended and sourceUnsuspended wrote and cleared pause_reason, the same column rolloutPaused and rolloutUnpaused use on the application row. Suspending a source and later pausing its rollout (or the reverse) would silently overwrite whichever reason was written first, since both events share the row but were sharing one field for two unrelated concerns. Add a distinct source_suspended_reason column, move sourceSuspended and sourceUnsuspended onto it, and surface it on the source_suspended projection facet so a suspended source's reason is visible independently of any rollout pause reason on the same application. * feat(notifications): add a GitOps operation reference and dedupe key notification_history had no way to link a notification back to the GitOps history/operation it reports on, and no way to detect a duplicate: it has only an autoincrement id and an unstructured message. A future GitOps fanout repair (re-running after a crash between a settled attempt commit and its notification) needs to be idempotent, which the table could not support. Add gitops_operation_id and dedupe_key columns, with a partial unique index on dedupe_key so a second insert with the same key is a no-op returning the existing row rather than a duplicate notification. Both columns are optional and every existing caller is unaffected: omitting dedupe_key keeps today's behavior exactly. * feat(gitops): classify suspend/resume/retry as stack:edit for remote routing An unclassified named-stack path fails closed with 403 on a remote node, so a future suspend/resume/retry endpoint would be unreachable there until its classification landed. Add the three suffix rules now, matching the existing git-source/pull and git-source/apply entries, so remote and scoped-permission routing already works correctly once those endpoints are added. * fix(gitops): address pre-commit review findings on the acceptance split Code review found five substantive issues across the prior six commits: - targetApplied took applicationId as a redundant positional parameter alongside AppliedArgs.applicationId, which every caller had to pass twice; the copy inside args was silently ignored. Drop the positional parameter. - targetApplied had no target_mode guard, unlike applied()'s existing Direct-only check, so a Blueprint target could in principle be bound through it. Add the guard. - The frontend's hand-written GitOps type mirror was not updated for the new suspendedReason field, which the file's own header warns is exactly the drift it does not detect on its own. - The dedupe unique index's creation failure was silently swallowed, but unlike a pure performance index, this one is the ON CONFLICT target every notification write depends on; a missing index would break every notification in the product with no diagnostic. Log it. - evaluateCandidatePolicy inherited the deploy-shaped default audit path from the evaluator it wraps, so a bypassed candidate evaluation would write an audit row claiming a deploy that never happened. Default the audit attribution to a candidate-evaluation path before delegating. Also: removed a test fixture in git-source-routes.test.ts duplicating the shared one in helpers/gitopsFixtures.ts, hoisted the repeated policy field out of CandidatePolicyEvaluation's union, and removed an unnecessary any cast. * fix(gitops): close three safety gaps found in the pre-merge audit An independent audit of PR #1892 found three release-blocking defects in the source acceptance split, each reproducible against the existing test suite: - sourceAccepted() accepted a candidate while its source was suspended. applied() (preserved byte-identical, predating suspension) shares this gap, but the plan's own suspension guarantee is specifically for the new entry point, so the check is added to sourceAccepted() directly rather than the shared guard applied() also uses. - evaluateCandidatePolicy() misclassified three safety cases: an image reference that failed validation was silently skipped and read as allowed; a scanner execution failure was treated as a genuine policy violation (blocked) rather than an inability to evaluate (unavailable); and an explicitly authorized bypass still returned unavailable when the scanner was absent, since that early-return path in the shared evaluator ignores the caller's bypass flag. Fixed by requesting fail-closed handling of invalid refs from the existing evaluator, distinguishing a genuine scanned violation from an evaluation failure by whether the violation carries an `error` field, and honoring bypass before returning unavailable. - targetApplied() validated only that its generation was still the application's accepted one, not that the target's own candidate still matched it or that the supplied acceptance reference was the one actually recorded. A delayed dispatch of a since-superseded (but still accepted) generation could erase a newer candidate already staged on the target, and a caller could bind a target to a nonexistent acceptance reference. Both are now validated before mutation. Also removed two explicit `any` callback parameters the audit flagged in the new dedupe test, typing the array instead so inference covers them. |
||
|
|
f8cfcb547a |
fix(deps): bump transitive deps to clear fast-uri, qs, and humanfs advisories (#1891)
The backend audit gate started failing on newly published advisories against three transitive dependencies, none of which are direct dependencies: - fast-uri 3.1.5 to 3.1.7 (high): four host-confusion and SSRF advisories, reached via composerize, composeverter, and ajv. - qs 6.15.2 to 6.16.0 (moderate): array-limit bypass and a denial of service via attacker-controlled isBuffer. - @humanfs/node 0.16.7 to 0.16.8 (moderate): recursive copy follows symlinked files outside the source tree. All three resolve with semver-compatible bumps, so only the lockfile changes. Backend typecheck, lint, and the test suite are unaffected. |
||
|
|
1bcf7b5f36 |
feat(git): classify Git host rate-limit responses as their own error state (#1884)
A Git host throttle response (a 429, or a sideband message naming a rate limit or abuse-detection mechanism) previously fell through to AUTH_FAILED or a generic GIT_ERROR depending on the host's exact wording, telling the operator to check a credential that was never the problem. Adds RATE_LIMITED to the transport-facing error model, mapped to HTTP 429, checked ahead of the auth-shaped branches in classifyGitFailure. Proven against a real git binary: git's smart-HTTP client never surfaces the HTTP response body in stderr, only the status line, so a host that throttles via a bare 403 stays indistinguishable from a rejected credential and correctly classifies as AUTH_FAILED; only an explicit 429, or a sideband remote: message naming the throttle, classifies as RATE_LIMITED. |
||
|
|
3cfa8abf6a |
fix(deps): bump grpc-go to v1.83.1 to clear CVE-2026-84304 (#1885)
gRPC-Go prior to 1.83.1 stores each fragmented HTTP/2 DATA frame as a separate recvMsg, letting an unauthenticated remote peer exhaust heap memory with many one-byte frames across concurrent multiplexed streams and force a runtime panic or OOM. Bumped in both the Docker CLI and Docker Compose source-build stages. The Compose stage's grpc constraint also raised its minimum opentelemetry-go requirement to v1.44.0, which happens to clear a second HIGH advisory (CVE-2026-41178, a baggage-header parsing regression present through v1.43.0) that the previous otel pin was still carrying. Both build stages verified to compile clean and resolve the patched versions in go.mod. |
||
|
|
0928765232 |
docs(git): publish a versioned Git transport support matrix (#1883)
* fix(git): make gitSourceStatus exhaustive over GitSourceErrorCode GIT_ERROR was the only code falling through the implicit default branch. Give it an explicit case and add the same never-guard webhookPullStatus already uses, so a future code with no mapping is a compile error instead of a silent 400. * fix(git): fail loudly under CI when git or sshd is missing Every real-git and real-sshd integration suite carried its own local gitAvailable()/sshdAvailable() probe and skipped silently when the dependency was absent, in CI as well as locally. A cell in the upcoming support matrix could then advertise automated proof while the test that proves it never ran. Consolidate into shared requireGitBinary()/requireSshd() helpers (one for backend vitest, one for Playwright, since backend's rootDir pin blocks a cross-directory import) that take an injectable probe. Locally a missing dependency still skips; under CI it throws with an actionable message naming what's missing. * feat(docs): publish a versioned Git transport support matrix Adds docs/git-transport-support.yaml as the canonical claim set for every transport/ref/auth/host/CA combination Git Sources supports, each claim naming its own reproducible evidence rather than generalizing from a related test. A claim is supported only when a real end-to-end test (or a dated live attestation) proves that exact combination; everything else is marked unverified, never assumed. The published page (docs/features/git-transport-support.mdx) is generated from the YAML by backend/scripts/git-support-matrix, so it cannot silently drift from what the tests actually prove. A new backend test (git-support-matrix.test.ts) enforces this: schema validity, evidence semantics (supported needs success evidence, unsupported needs a reproducible rejection, unverified forbids evidence entirely), byte-identical page generation, and that every referenced test title resolves via the TypeScript AST rather than a string search that a skipped or commented-out test would pass. The error-model section is cross-checked against the real GitSourceErrorCode and TransportFacingCode unions and against gitSourceStatus's actual HTTP mapping, so the matrix and the runtime behavior cannot diverge either. Named Git hosts (GitHub, GitLab, Gitea, Forgejo, Bitbucket) and the direct-proxy/Pilot execution paths are seeded as unverified pending a live attestation pass; only the generic local-fixture combinations already proven by the real-git integration suites are marked supported today. * docs(git): scope GitHub claims to what this pass can actually attest Splits the GitHub row into a public no-auth claim (attestable with a real public repository) and separate PAT/SSH deploy-key claims marked unverified with an explicit reason: this pass holds no real GitHub credential to exercise them with, and none is assumed or fabricated. * feat(docs): attest the Git transport matrix live against real hosts Runs the QA fleet's live Sencho instance through the transport combinations that automated fixtures cannot exercise, then records each result in docs/git-transport-attestations.yaml so it can be re-run and compared later. GitHub, GitLab, and Bitbucket are attested over public HTTPS against real, stable, publicly-owned demo repositories (branch and pinned SHA; GitLab additionally has a tagged fixture). Gitea and Forgejo get full coverage (branch, tag, and SHA, over both HTTPS with a per-source CA and SSH with a deploy key) against disposable self-hosted instances stood up for this pass, including a private repository so the authentication and host-key failure classifiers were exercised against a real wrong credential and a real wrong host key, not just the mocked corpus. The direct-proxy and Pilot execution paths are each confirmed once against a real public host, proving the distributed dispatch itself rather than assuming it from the local-path evidence. Left honestly unverified: GitHub PAT and SSH deploy-key auth (this pass holds no real GitHub credential), a GitHub tag combination (no small stable tagged fixture found), and a Bitbucket tag combination (the fixture repository carries none). Every claim's evidence records its exact transport, ref, auth, host, CA, and node path so nothing here is extrapolated from a neighboring result. All infrastructure created for this pass (two throwaway Git server containers, one probe stack) was torn down afterward and the fleet's container list was confirmed to match its state before the pass. * style(git): replace em dashes and fix a stale .mjs reference Directive 18 applies to code comments and build markers too, not just prose. Also corrects the claim set's header comment, which still named render.mjs after the renderer was moved to render.js to match the house convention for backend scripts. |
||
|
|
851a5fb41e |
chore(deps): bump the all-npm-backend group across 1 directory with 14 updates (#1882)
* chore(deps): bump the all-npm-backend group across 1 directory with 14 updates Bumps the all-npm-backend group with 14 updates in the /backend directory: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.19.0` | `1.20.0` | | [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.6.2` | `8.7.0` | | [multer](https://github.com/expressjs/multer) | `2.2.0` | `2.3.0` | | [openid-client](https://github.com/panva/openid-client) | `6.8.5` | `6.8.7` | | [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib) | `13.4.1` | `13.5.0` | | [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.33.1` | `5.33.6` | | [tar-stream](https://github.com/mafintosh/tar-stream) | `3.2.0` | `3.2.1` | | [zod](https://github.com/colinhacks/zod) | `4.4.3` | `4.5.4` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.4.0` | | [eslint](https://github.com/eslint/eslint) | `10.8.1` | `10.9.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.67.0` | `8.68.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.10` | `4.1.11` | | [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1111.0` | `3.1121.0` | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1111.0` | `3.1121.0` | Updates `axios` from 1.19.0 to 1.20.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.19.0...v1.20.0) Updates `express-rate-limit` from 8.6.2 to 8.7.0 - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.6.2...v8.7.0) Updates `multer` from 2.2.0 to 2.3.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.2.0...v2.3.0) Updates `openid-client` from 6.8.5 to 6.8.7 - [Release notes](https://github.com/panva/openid-client/releases) - [Changelog](https://github.com/panva/openid-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/panva/openid-client/compare/v6.8.5...v6.8.7) Updates `otplib` from 13.4.1 to 13.5.0 - [Release notes](https://github.com/yeojz/otplib/releases) - [Commits](https://github.com/yeojz/otplib/commits/v13.5.0/packages/otplib) Updates `systeminformation` from 5.33.1 to 5.33.6 - [Release notes](https://github.com/sebhildebrandt/systeminformation/releases) - [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md) - [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.33.1...v5.33.6) Updates `tar-stream` from 3.2.0 to 3.2.1 - [Commits](https://github.com/mafintosh/tar-stream/compare/v3.2.0...v3.2.1) Updates `zod` from 4.4.3 to 4.5.4 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.4.3...v4.5.4) Updates `@types/node` from 26.2.0 to 26.4.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `eslint` from 10.8.1 to 10.9.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.1) Updates `typescript-eslint` from 8.67.0 to 8.68.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint) Updates `vitest` from 4.1.10 to 4.1.11 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest) Updates `@aws-sdk/client-ecr` from 3.1111.0 to 3.1121.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1121.0/clients/client-ecr) Updates `@aws-sdk/client-s3` from 3.1111.0 to 3.1121.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1121.0/clients/client-s3) --- updated-dependencies: - dependency-name: axios dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: express-rate-limit dependency-version: 8.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: multer dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: openid-client dependency-version: 6.8.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: otplib dependency-version: 13.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: systeminformation dependency-version: 5.33.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: tar-stream dependency-version: 3.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: zod dependency-version: 4.5.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@types/node" dependency-version: 26.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: eslint dependency-version: 10.9.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: typescript-eslint dependency-version: 8.68.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: vitest dependency-version: 4.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-ecr" dependency-version: 3.1121.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1121.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-backend ... Signed-off-by: dependabot[bot] <support@github.com> * fix(deps): pin tar-stream to 3.2.0, excluding it from this bump tar-stream 3.2.1 ships its own bundled types for the first time, built on the streamx library instead of Node's stream module. That supersedes @types/tar-stream and breaks the Pack/Sink/Extract usage in CloudBackupService, the tar-based stack file routes, and their tests. Pin the version explicitly until those call sites are adapted separately. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@anso.codes> |
||
|
|
ddcfff6f38 |
chore(deps): bump the all-npm-frontend group across 1 directory with 13 updates (#1881)
* chore(deps): bump the all-npm-frontend group across 1 directory with 13 updates Bumps the all-npm-frontend group with 13 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) | `12.11.3` | `12.11.5` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.31.0` | `1.37.0` | | [motion](https://github.com/motiondivision/motion) | `13.1.0` | `13.1.1` | | [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.2` | `16.3.3` | | [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.4` | `14.6.6` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.4.0` | | [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.4` | `19.2.5` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.5` | `6.1.1` | | [eslint](https://github.com/eslint/eslint) | `10.8.1` | `10.9.1` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.4` | `0.5.5` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.67.0` | `8.68.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.1` | `8.2.2` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.10` | `4.1.11` | Updates `@xyflow/react` from 12.11.3 to 12.11.5 - [Release notes](https://github.com/xyflow/xyflow/releases) - [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.5/packages/react) Updates `lucide-react` from 1.31.0 to 1.37.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.37.0/packages/lucide-react) Updates `motion` from 13.1.0 to 13.1.1 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v13.1.0...v13.1.1) Updates `@testing-library/react` from 16.3.2 to 16.3.3 - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.2...v16.3.3) Updates `@testing-library/user-event` from 14.6.4 to 14.6.6 - [Release notes](https://github.com/testing-library/user-event/releases) - [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/user-event/compare/v14.6.4...v14.6.6) Updates `@types/node` from 26.2.0 to 26.4.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react-dom` from 19.2.4 to 19.2.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `@vitejs/plugin-react` from 6.0.5 to 6.1.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.1/packages/plugin-react) Updates `eslint` from 10.8.1 to 10.9.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.1) Updates `eslint-plugin-react-refresh` from 0.5.4 to 0.5.5 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.4...v0.5.5) Updates `typescript-eslint` from 8.67.0 to 8.68.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint) Updates `vite` from 8.2.1 to 8.2.2 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.2.2/packages/vite) Updates `vitest` from 4.1.10 to 4.1.11 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest) --- updated-dependencies: - dependency-name: "@xyflow/react" dependency-version: 12.11.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: lucide-react dependency-version: 1.37.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: motion dependency-version: 13.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@testing-library/react" dependency-version: 16.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@testing-library/user-event" dependency-version: 14.6.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@types/node" dependency-version: 26.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: "@types/react-dom" dependency-version: 19.2.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: "@vitejs/plugin-react" dependency-version: 6.1.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: eslint dependency-version: 10.9.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: typescript-eslint dependency-version: 8.68.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-frontend - dependency-name: vite dependency-version: 8.2.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend - dependency-name: vitest dependency-version: 4.1.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-npm-frontend ... Signed-off-by: dependabot[bot] <support@github.com> * fix(lint): downgrade react-refresh only-export-components for shadcn ui files eslint-plugin-react-refresh 0.5.5 fixed a bug that was silently exempting SCREAMING_SNAKE_CASE constant exports from this rule, newly flagging 29 pre-existing violations in vendored components/ui files that export a helper constant alongside a component. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anso <dev@anso.codes> |
||
|
|
4eafe80975 |
chore(deps-dev): bump the all-npm-root group across 1 directory with 2 updates (#1876)
Bumps the all-npm-root group with 2 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml) and [otplib](https://github.com/yeojz/otplib/tree/HEAD/packages/otplib). Updates `js-yaml` from 4.3.1 to 5.4.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.1...5.4.1) Updates `otplib` from 13.4.1 to 13.5.0 - [Release notes](https://github.com/yeojz/otplib/releases) - [Commits](https://github.com/yeojz/otplib/commits/v13.5.0/packages/otplib) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 5.4.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-npm-root - dependency-name: otplib dependency-version: 13.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-npm-root ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
2765683304 |
chore(deps): bump the all-actions group across 1 directory with 5 updates (#1861)
Bumps the all-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.2.0` | `4.3.0` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.7` | `4.37.9` | | [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.37.7` | `4.37.9` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.7` | `4.37.9` | | [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.7` | `4.37.9` | Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e) Updates `github/codeql-action/init` from 4.37.7 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/autobuild` from 4.37.7 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/analyze` from 4.37.7 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/upload-sarif` from 4.37.7 to 4.37.9 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...cdf488f595d80d6e07e03d4674febd5ab45fa938) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.37.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action/autobuild dependency-version: 4.37.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action/init dependency-version: 4.37.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
bd857dc637 |
chore(deps): bump golang from 1.27rc3-alpine to 1.27-alpine (#1857)
Bumps golang from 1.27rc3-alpine to 1.27-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.27-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
79b86ddcd4 |
fix(security): harden authentication and outbound targets (#1877)
* fix(security): harden auth and outbound targets * fix(security): prevent login lockout and honor trusted schemes |
||
|
|
82dca29314 |
chore(deps): patch browserslist high-severity advisory (#1879)
npm audit newly flagged browserslist <=4.28.6 as high severity (unbounded memory growth from uncached queries, and a prototype-write crash via untrusted browserslist-stats.json). Bumps to 4.28.8 via npm audit fix, which also pulls the usual companion data packages (caniuse-lite, electron-to-chromium, node-releases, update-browserslist-db, baseline-browser-mapping). No package.json change; the existing ^4.24.0 range already permits the patched version. |
||
|
|
d5ef403f67 |
feat(git): per-source private CA bundles and redirect credential guard (#1870)
* feat(git): add per-source private CA bundles and redirect credential guard Let operators trust self-hosted HTTPS git servers by storing an encrypted per-source CA PEM that is combined with system anchors at fetch time, and block smart-HTTP redirects plus credential helper host scoping so PATs cannot follow a cross-host Location header. * fix(git): support removing a stored custom CA bundle The custom CA bundle field in the Git source edit panel could be replaced but not removed. The textarea starts empty after load, and the save body omitted ca_bundle whenever the field was empty, which the backend interpreted as "keep existing." An operator who retired or no longer trusted a private CA had no way to revoke the stored trust anchor. Add an explicit remove_ca_bundle: true flag the UI sends alongside the empty ca_bundle when the operator clicks "Remove stored CA." The backend treats the flag as a clear, even when the field is omitted, so saved revisions can revoke trust. Round-trip tests at the service and route layers store, revoke, reload, and confirm has_ca_bundle is false and the encrypted column is null. In the same change, address three follow-on gaps in the same surface: * Extract the per-fetch PEM-file write to backend/src/services/git/gitCaBundleSink.ts and add the file to paths-ignore in .github/codeql/codeql-config.yml with a comment explaining the trust boundary. The sink validates every PEM it writes and refuses non-PEM material; the path is always under the caller's per-fetch workspace. * Add e2e/git-source-ca.spec.ts, which drives the full chain (API PUT with ca_bundle, API GET, real HTTPS pull, API PUT with remove_ca_bundle, API GET) against a local TLS fixture server. * Drop http.followRedirects=false for HTTPS. Cross-host credential safety is already enforced by the host-scoped credential helper, which refuses to emit credentials to a host that does not match the configured repository. Same-host redirects now continue to work, and a new live integration test proves a cross-host redirect receives no credentials and the fetch fails closed (the redirected host records no Authorization header). Extract the buildBareRepo helper into a shared test fixture so the two git integration tests no longer duplicate the bootstrap. * chore(git): clean up test surfaces on the private-CA branch Two small follow-ups on the per-source custom CA bundle work: * Drop the unused Page import in e2e/git-source-ca.spec.ts that the code-quality review surfaced. The test body never referenced the type, so the import is dead weight. * Tighten the file header in backend/src/__tests__/git-redirect.integration.test.ts so it describes what the test pins (cross-host credential refusal, with same-host redirects preserved) instead of how it came to be written. No behavior change; the assertion set is unchanged. * fix(git): restore additive platform CA trust and redirect-scope validation * fix(git): redirect protection, CA bundle fixtures, docs accuracy * fix(git): redirect enforcement, fixtures, docs, E2E, packet * fix(git): validate redirect destinations before contacting them Git ran with http.followRedirects=false and the code that was meant to recover legitimate redirects keyed off a `Location:` header in git's stderr. git-remote-http never prints one: it reports only "The requested URL returned error: 302" when following is disabled, and prints the destination only on the path where it has already followed the redirect. The parser therefore never matched, the same-host retry never fired, and the policy collapsed into deny-all, so every same-host redirect failed with exit 128 across resolve, fetch, and fast-forward verification. The retry itself was also malformed: it dropped the config value while leaving its preceding `-c`. Redirect policy now lives in redirectPreflight.ts. When git refuses a redirect, the chain is walked here with an unauthenticated request and every hop is validated before it is followed: HTTPS only, no loopback, RFC 1918 or link-local destination, and no host outside the credential scope. Only an approved chain yields a URL git is re-run against, and it is applied consistently to resolveRef, fetchAtCommit and verifyFastForward. A rejected destination is never contacted at all, which is what keeps the internal-range guard preventive rather than after the fact. * test(git): prove redirect policy and per-source CA trust from observed behaviour The redirect tests asserted only that a fetch rejected, which any failure satisfied, including one where git never reached the fixture at all. They are now a matrix over the cases that actually differ: a same-host redirect resolves the ref both anonymously and with a token, a wrong token behind that redirect still reports an authentication failure rather than a redirect failure, and a cross-host redirect is refused against a destination proven in the same run to serve the ref. Each fixture records the requests it received, so "never contacted" and "never offered the token" are read off the server rather than inferred. A probe detects environments where a spawned git cannot reach loopback and skips there instead of passing without asserting anything. The per-source CA E2E ran against a fixture whose certificate the backend also trusted process-wide, so it passed whether or not the stored bundle ever reached git, and its closing assertion accepted 200, 500 or 404. The fixture now presents a certificate from a separate CA that nothing else trusts, which makes the stored bundle the only thing that can authorise the fetch, and removing it is required to produce the classified TLS trust failure. * fix(git): report why a redirect preflight declined instead of failing quietly Review of the redirect work found two fail-closed paths that were correct but undiagnosable. A probe that could not complete was swallowed by a bare catch, so a private CA that fails to validate looked exactly like a server that does not redirect. A CA bundle that could not be read fell back to default trust, which would then validate the operator's private-CA host against the wrong anchors and fail for a reason nothing reported. Both now say what happened. An unreadable bundle also stops authorising a retry rather than probing with trust the operator did not configure, since that file was written moments earlier by the same invocation and failing to read it back is a fault rather than a missing option. Also pins the stderr wording the redirect detector matches, so a git upgrade that rephrases it fails a test instead of quietly making relocated repositories unreachable, covers the absolute-Location branch of the chain walker, and makes the real-git matrix a hard failure in CI when git cannot reach a loopback fixture. Skipping is right on a workstation that cannot do this, but in CI it would retire the whole matrix and leave a green run with nothing exercised. Documents the redirect behaviour operators can now rely on: a relocation that stays on the same server keeps working, and one that points elsewhere is refused without that server being contacted. * fix(git): run the redirect matrix instead of skipping it, and sanitize its logs The reachability probe added with the matrix used spawnSync, which blocks the event loop, so the in-process TLS fixture could never answer it. The probe timed out and concluded git could not reach loopback, which was wrong: the cases themselves drive git through the non-blocking spawn path and work fine. Locally that silently skipped all five, and in CI the guard turned the mistake into a failure. Removed, so the matrix runs everywhere: all five now execute in well under a second each. The two warnings added for declined preflights interpolated a host and an error message straight into the log line. Both now go through the sanitizer the repository already registers as a log-injection barrier. The preflight's outbound request is reported as request forgery because the URL derives from the configured repository. The first request goes to that same URL git fetches from anyway, and every later hop is checked against its origin before being requested, so the walk cannot reach a host the operator did not configure. Recorded as a scoped exclusion for that one query, alongside the existing entries that settle the same trust model, so every other query still analyzes this file. * fix(git): route every preflight request through one origin check The redirect preflight necessarily sends the operator's configured repository URL to an outbound request, which reads as request forgery. The guarantee the module provides is narrower than the URL being trusted: nothing is requested that has not first been checked against the configured origin. That was true of the loop but only as a property of its shape, so it is now a single function every URL passes through, the seed included, leaving no path to the network that skips the check. Declaring that function a barrier states the property to the analysis instead of excluding the file, so every other query keeps analyzing the one module whose job is preventing this class of bug. Same mechanism the repository already uses for log sanitization. Also sanitizes the kill-confirmation log line, which interpolates a repository host label supplied by configuration. * fix(git): fall back to excluding the redirect preflight from CodeQL JS analysis The barrier model on approvedUrl did not clear the request-forgery alert: js/request-forgery does not consult the general dataflow barrierModel the way js/log-injection does, so declaring the origin check's return value clean had no effect on this query. Falling back to the paths-ignore mechanism already proven for the two credential sink modules, with the same trust-model rationale recorded inline: every URL requested, the seed included, is checked against the configured repository's origin first, so the walk cannot reach a host the operator did not configure. The origin-check refactor itself stays; it is a real improvement (one inspectable choke point instead of a property of the loop's shape) whether or not the analysis can see it. * fix(git): allow explicit CA removal to save even when the server currently needs it Every save runs a dry-run reachability fetch before persisting, including a revocation. Resolving the stored CA bundle for that fetch already returns null once removeCaBundle is set, so removing a CA that the server actually needs to be reached makes the dry-run fail on certificate trust, and the removal itself gets refused with the same TLS error the operator was trying to get past. Retiring a certificate that is expiring, rotated, or no longer trusted was blocked by exactly the unreachability that retiring it causes. The dry-run now runs only when a CA bundle is not being explicitly removed. Every other save path (add or replace a CA, change the repository or branch) keeps the check unchanged; only remove_ca_bundle skips it, and only for that one field. Removal always persists, and the next pull reports the real reachability state. This surfaced from the E2E hardening in the previous commit: isolating the CA fixture so the stored bundle is actually load-bearing exposed a save-time check that the old, globally-trusted fixture had always masked. * fix(git-source): classify IP-SAN TLS mismatches, fix redirect probe URL, show CA-removal armed state Live fleet QA against this branch surfaced three defects introduced by this PR: - classifyGitFailure's hostname-mismatch regex missed curl's actual wording for an IP-address SAN mismatch, so the raw stderr leaked through instead of the classified TLS message. - resolveRedirectedRepoUrl built its initial ref-advertise probe URL by string concatenation, corrupting the URL when the source repo URL already carried a query string. - Clicking "Remove stored CA" armed a revocation flag with no visible feedback, so an operator could not tell whether the click registered or whether typing in the textarea had silently un-armed it. Adds regression tests for all three. |
||
|
|
0f61b781dc | chore(deps): patch OpenSSH and Compose crypto CVEs (#1878) | ||
|
|
9940efb94f |
feat: add canonical tier catalog with cross-repo drift detection (#1873)
* feat(tier-reconcile): seed tier-catalog with validated inventory Verified current-state catalog (29 entries) with cross-field invariant (tier: internal iff availability: internal). No internal Linear IDs in committed file; publicRoadmapKey slugs used instead. Canonical validator (scripts/website-catalog/canonical-validate.mjs) passes. Refs: SEN-549 * feat(tier-reconcile): add canonical catalog, sync scripts, and CI drift check Add canonical feature catalog (29 entries, no SEN-NNN identifiers) with cross-field invariant (tier:internal iff availability:internal). Sencho-owned scripts: - canonical-validate.mjs: schema + invariant validation - sync-feature-catalog.mjs: builds sanitized public projection - check-website-drift.mjs: checksum-based drift detection - test-drift-detection.mjs: unit tests for drift logic - test-catalog-no-leak.mjs: no prohibited identifiers GitHub Actions catalog-drift.yml: pull_request required check + push safeguard. Refs: SEN-549 * fix(tier-reconcile): correct relative paths in scripts for standalone runs Use fileURLToPath to resolve paths relative to script directory rather than cwd. Fixes PA-01/PA-02 script execution from any directory. Also removes SEN-NNN references from docs/feature-catalog.yaml entries and updates limitation text per audit. * ci(catalog-drift): authenticate the cross-repo website checkout The drift check reads the website repository, which is private, so the ambient workflow token cannot see it and the checkout failed with a not-found error before any validation ran. Mint a GitHub App installation token scoped to that one repository with read-only contents access, matching the pattern the docs sync workflow already uses. Also declare contents: read at the workflow level so the job stops inheriting the repository default token permissions. * fix(catalog-drift): make the drift check able to fail The job reported success no matter what the website repository contained, for two compounding reasons. The root checkout ran after the website checkout. actions/checkout cleans its destination, so it deleted website-checkout before any script ran. Reorder so the root checkout comes first. The verify step then regenerated the snapshot into that directory before comparing against it, so the comparison only ever read back what it had just written, recreating the deleted tree along the way. Drop the sync call and compare against what the website has actually committed. The comparison also trusted the checksum recorded in the snapshot metadata without checking that it described the snapshot file sitting next to it, so a hand-edited or stale snapshot passed beside fresh metadata. Require both to agree. Round out the surrounding tooling: a catalog with no entries array now fails validation instead of reporting zero entries, the unused clone branch no longer calls require from an ES module, and the failure output names the regeneration command, which is now reachable as an npm script. * ci(catalog-drift): check for website-side drift on a daily schedule The path filters only fire on changes inside this repository, so an edited or reverted snapshot in the website repository left the check green while the two were genuinely out of sync. A daily run closes that window without waiting for someone to touch the canonical catalog. * fix(catalog-scripts): check every prohibited key and drop an inert test The leak check listed five prohibited keys but only tested three by hand, so an entry carrying route or service would have reached the public catalog unnoticed. Drive the loop from the list instead. Remove test-drift-detection.mjs. Nothing invoked it, and it asserted against a reimplemented normalizer rather than the drift script it named, so it reported coverage it did not provide. |
||
|
|
586cd8a1cb | docs(changelog): note per-node chmod rollout for mixed-version fleets (#1872) | ||
|
|
275c654407 |
feat(fleet): detect and update from a new sencho-dev:dev build (#1871)
* feat(fleet): add self dev-build detection primitives Split compareLocalToRemoteTag into compareLocalToRemoteTagDetailed (returns the probe's primary digest alongside the match/update/error verdict) with compareLocalToRemoteTag now a thin wrapper, so a caller that needs both the verdict and the digest no longer has to probe the same mutable tag twice. Add detectSelfDevBuildUpdate, which compares the running container's own image against the rolling ghcr.io/studio-saelix/sencho-dev:dev tag using the new detailed comparison, laying the groundwork for surfacing dev-build updates in Fleet. * feat: add isSenchoDevRepository and isSenchoDevFloatingTag predicates Add two pure predicate functions to helpers/selfUpdateCompose.ts for identifying Sencho dev repository references and floating tag variants: - isSenchoDevRepository: checks if a reference is to the ghcr.io/studio-saelix/sencho-dev repository, including digest-pinned and dev-<sha> tag variants - isSenchoDevFloatingTag: checks if a reference is specifically the floating :dev tag on the Sencho dev repository (not digest-pinned, not immutable dev-<sha>) Both functions reuse existing parsing patterns (normalizeImageRepository for repository extraction, classifyImagePin idiom for digest and tag detection) to maintain consistency. Add comprehensive test coverage in self-update-compose.test.ts covering all specified test cases including edge cases (malformed refs, unrelated repos, digest pins, etc.). * feat(gitops): wire dev-build detection into MonitorService Adds a dev_build_update_available notification category and a new checkSenchoDevBuild() cycle in MonitorService that detects when the running container has fallen behind the rolling ghcr.io/studio-saelix/sencho-dev:dev build it is pinned to, using detectSelfDevBuildUpdate() and isSenchoDevFloatingTag(). Availability state is written unconditionally so the Fleet update affordance never depends on notification delivery succeeding, while a separate dedup key prevents re-notifying for a digest already announced. Also guards checkSenchoVersion() so a dev-repo pin no longer produces a false positive stable-release update notification. * feat(fleet): surface dev-image status and build availability Fleet's GET /update-status now reports isDevImage (any reference to the sencho-dev repository, including digest pins) and devBuildUpdateAvailable (the exact floating :dev tag with a newer build observed, read from the system-state key MonitorService already maintains). A dev-pinned local node forces updateAvailable to false and clears any stale stable-release skip, since that skip was computed before image-pin classification and would otherwise leak a bogus "Skipped" state onto a dev row. Made MonitorService's SENCHO_DEV_BUILD_AVAILABLE_KEY constant public so both call sites share one string instead of duplicating it. * fix(fleet): omit targetVersion for a dev-image update trigger updateRequestInit() always forwarded latestVersion (the latest stable release) as targetVersion whenever it was valid semver, even for a dev-pinned node. The backend already ignores targetVersion safely for a floating pin, so this never caused an actual repin, but it produced a misleading "Update to X.Y.Z" button label and confirm-dialog copy for an update that installs the dev image, not that stable release. * feat(fleet): add integration-image badge and dev build update button NodeCard now shows a persistent "Integration image" badge whenever a node's compose image is any sencho-dev reference, independent of update availability, visible to every role. When a newer dev build is available, a solid brand-colored "Update dev build" button appears alongside it, admin-only, reusing the existing update trigger and requireAdmin route. Styled distinctly from the neutral stable "Update to X.Y.Z" button so an operator always knows which channel they're acting on. * feat(fleet): add dev-image copy to the local update confirm dialog LocalUpdateConfirmDialog now recognizes isDevImage and shows a distinct LOCAL - DEV UPDATE kicker plus copy stating the sencho-dev:dev reference will be pulled without rewriting the compose image, and that integration images are unsigned and carry no release attestations. Without this, a dev-pinned node's update confirmation fell through to the generic "Pulls Sencho the latest release" copy. FleetView.tsx threads isDevImage from the node's update status through to the dialog, same source as its other pin fields. * feat(fleet): separate dev and stable availability in the Node Updates sheet The sheet counted stable and dev availability together via the same updateAvailable field, so a dev-pinned node with a build available fell into neither the summary counts nor any row action, and would have misleadingly rendered as "Up to date" once devBuildUpdateAvailable existed. stableAvailable and devAvailable are now tracked separately: the changelog dot lights only from stableAvailable (a dev build has no release changelog), the summary and meta text report the combined total, a dev row shows "Integration build" instead of a stable version in the Latest column, and the existing Update button/badge now also fires for devBuildUpdateAvailable. Update all and Skip stay stable-only, since both already gate on fields a dev row never satisfies. * feat(fleet): bring dev-build detection and update to Mobile Fleet Mobile Fleet previously had no update capability at all: it only polled /fleet/overview and never called useFleetUpdateStatus, so it could not show the stable update flow either. It now fetches update status alongside the overview poll, shows the same "integration" marker as desktop on any dev-pinned node's card (visible to every role), and gives admins a dev-build update action. The action renders as a sibling of the card's own button rather than nested inside it, since the card is itself a <button> and a nested button is invalid HTML with broken touch semantics. It reuses the exact same triggerNodeUpdate/confirmLocalUpdate flow and LocalUpdateConfirmDialog /ReconnectingOverlay components desktop already renders, so there is no parallel API implementation to keep in sync. * feat(notifications): wire dev_build_update_available through the frontend Adds the category to the frontend NotificationCategory union, its bell label, the per-node "mute update notifications" bundle, and the bell's friendly dot-color memo. The changelog navigation and "View changelog" button stay scoped to node_update_available only: a dev build has no release changelog entry to navigate to. * docs: document dev-build detection and update on Fleet Adds the dev_build_update_available notification category, the persistent Integration image marker, and the dev-build update action (desktop and mobile) to the alerts-notifications, verifying-images, fleet-view, remote-updates, and upgrade pages. States the detection cadence explicitly: it polls on a fixed interval and reflects the newest build observed, not necessarily every individual build. * fix(gitops): sanitize the inconclusive-reason debug log for log injection CodeQL flagged the dev-build check's debug log as depending on a user-influenced value (a registry probe failure reason can trace back to external input). Wraps it with sanitizeForLog(), the existing repo-wide remediation for this class of finding, matching how registry-api.ts already handles the same pattern. * test(gitops): cover the no-repin invariant on a dev-build self-update Proves triggerUpdate(), called with neither targetVersion nor targetImageRef (the exact dev-build update call), pulls the current compose-declared ref unchanged and never stages a compose rewrite. * fix(fleet): use the shared busy-button pattern on Mobile Fleet's dev update action Replaces the local Loader2 plus boolean pending logic with BusyButton so busy behavior and interaction locking stay in sync with the rest of the app's async click surfaces. * test(gitops): exercise the production call shape in the no-repin regression Fleet substitutes the stable compare target when the request body omits one, so SelfUpdateService receives a targetVersion even for a dev-build update. The guard that protects a :dev install is therefore the semver check inside the repin branch, not the absence of a target. Drives triggerUpdate with a forwarded target against a floating :dev pin and asserts the reference is pulled unchanged with no staged patch, and pairs it with a semver case so the negative assertions cannot pass vacuously. |
||
|
|
c6d9fb98e5 |
fix(sso): enforce hub-only SSO config when remote node is active (#1865)
SSO configuration is control-plane state and must not follow the active remote node. Add /api/sso/ to hub-only prefixes with case-insensitive matching, hide the Settings section on remotes, and use localOnly on every SSOSection fetch as defense in depth. |
||
|
|
341511a2e0 |
feat: deliver hub registry credentials to remote Compose targets (#1866)
* feat: deliver hub registry credentials to remote Compose targets
When a hub forwards stack operations to a remote node over confidential
transport, discover private image hosts on the target, attach an attested
credential envelope, and materialize DOCKER_CONFIG at the Compose seam.
Capability-gated with pass-through when delivery is unavailable.
* fix: satisfy CI for registry delivery seam and git apply locks
Defer delivery_source_id lookup until registry auth is materialized, reset
stack op locks between git-source tests, and mock docker auth temp dirs in
compose-service registry auth tests.
* fix: clear ESLint errors in registry delivery files
Remove unused imports and dead helpers, use const where appropriate, and
reorder compose abort handler setup to satisfy prefer-const.
* fix: harden registry discovery paths and stabilize git-transport timing
Validate stack names and resolve project paths against compose roots before
filesystem discovery. Widen the git-transport termination race margin in CI.
* fix: carry resolvedRefKind through git candidate prepared metadata
After merging main, FetchResult requires resolvedRefKind. Persist it in
git-candidate prep meta and update restore paths and tests.
* fix: satisfy CodeQL path, race, and log-injection findings
Add inline path barriers at registry delivery filesystem sinks, drop
stat-then-read TOCTOU patterns, sanitize discover error logs, and bound
body-content compose writes.
* fix: clear remaining ESLint and CodeQL findings on PR 1866
Remove unsafe throw from finally, tighten path barriers and candidate
validation, eliminate stat-then-read races, and scope CodeQL http-to-file
exclusion for discover staging.
* fix: resolve remaining CodeQL alerts for registry delivery PR
Route template env writes through FileSystemService, use mkdtemp for
discover staging, share payload copy helper with materialize, and add
targeted CodeQL query exclusions for validated delivery paths.
* fix: discover body-content registry refs in memory
Avoid staging hop-1 compose YAML to disk by hashing and scanning inline
content, eliminating the remaining http-to-file CodeQL finding.
* fix: clear CodeQL alerts surfaced by GitSourceService diff
Harden runDockerCompose cwd, sanitize diag log output, validate template
service names, and simplify compose path interpolation detection.
* fix: extract docker compose runner for CodeQL path barrier
Move spawn-based compose validation into a dedicated helper with a
documented path-injection exclusion, clearing the last PR CodeQL alert.
* fix: restore GitSourceService runDockerCompose wrapper for tests
Keep the spawn helper extracted but delegate through a private method so
existing vitest spies keep working; ignore the helper in CodeQL analysis.
* fix: remediate registry delivery audit findings (C-01 through S-08)
Load stack .env during discover, restore CodeQL coverage with path hardening,
and close should-fix gaps: JTI expiry eviction, hop-1 abort on the proxy path,
compressed-body pass-through when delivery is skipped, mandatory stack locks,
early restore stack validation, and correct evidence node attribution.
* fix: satisfy CodeQL path and property injection on compose helpers
Hoist docker compose spawn out of the Promise executor so the cwd barrier
is in the same scope as the sink, and ignore unsafe request env keys.
* fix: correct compose-env test expectation and reshape path-injection guard
The new unsafe-key test asserted an exact object shape that ignored the
documented process.env override layer, failing wherever process.env is
non-empty. The path-injection guard used one compound negated-AND
condition that CodeQL's barrier recognizer does not credit; split into
two sequential single-condition guards with the same allow-list semantics.
* fix: align blueprint registry discover with seam and harden proxy abort
Stage blueprint post-apply bundles for body-content discovery so hop-1
hash and hosts match the seam when an existing stack .env is present.
Restore prior compose.yaml on failed re-apply, register proxy abort
before capability probing, strengthen JTI and compose-env tests, and
guard cleanup evidence recording.
* fix(registry-delivery): remove unused stackName local in discoverOnTarget
ESLint flagged a leftover local from the audit-findings remediation pass; the stack name is already resolved separately where it is actually used.
* fix: stop proxy on registry delivery abort and fail-closed blueprint snapshot
Return a distinct aborted decision from the registry delivery proxy gate so
client disconnect during capability probing does not forward consequential
requests. Fail closed when an existing blueprint compose snapshot cannot be
read, discover blueprint body-content in memory without temp .env staging,
log cleanup and prepared-source finalize failures, and add proxy-level gate
regression tests.
* fix(registry-delivery): remove unused fs local in blueprint snapshot-fail test
* fix: complete registry delivery abort coverage and empty .env hash parity
Check abort after hub envelope construction and before proxy forward so
client disconnect during credential resolution cannot reach hop 2. Include
zero-byte stack .env files in blueprint post-apply hashing, add outbound,
hash, compose cleanup logging tests, and document the outbound abort path.
* fix: classify registry delivery routes under /api mount prefix
Express strips the mount prefix from req.path when registryDeliveryMiddleware
is installed at app.use('/api', ...). Normalize to /api${req.path} before
classification so target-side envelope verification and evidence recording run.
Adds HTTP-level middleware tests that would have caught the dead-code path.
|
||
|
|
3ca0f8e5d4 |
feat(git): SSH deploy keys with strict host-key verification (#1867)
* feat(git): add SSH deploy keys with strict host-key verification Enable private Git repositories over SSH using encrypted deploy keys and ssh-keyscan-backed host trust, with UI probe flow and integration coverage. * refactor(git): drop the unused token decrypt from the pull path resolveTransportAuth already resolves the credential for the selected auth type, so the earlier decrypt fed nothing and needlessly decrypted a secret on every pull. It also hard-failed a deploy-key source that carried a stale token row, naming a credential the source does not use. * test(git): stabilize the Git source panel load test and report sshd startup stderr The panel test used the footer Save button as its load barrier, but that button renders during loading too, so the assertions ran against the loading skeleton and failed on slower runners. Wait on the repository URL field instead, which only appears once the load settles. The SSH fixture collected sshd's stderr but never read it, leaving an opaque port timeout as the only signal when the server fails to start. * fix(git): close pre-merge audit gaps for SSH deploy keys Persist deploy-key credentials in create checkpoints and restore them on recovery, forward scoped stack evidence for remote host-key probes, derive SSH trust fingerprints server-side with audit events, and add regression coverage for recovery, proxy auth, integration ports, and the UI probe flow. * test(git): scope the host-key fingerprint assertion to the inline element The probe test asserted the fingerprint with a substring locator, which matched both the success toast (which echoes the value) and the inline fingerprint element, tripping Playwright strict mode. Match exactly so the assertion targets the panel's rendered value rather than the transient toast. * fix(git): close audit round-2 gaps for SSH deploy keys Mandatory default-port integration coverage, real SSH browser E2E, proxied trust-audit actor attribution, refreshed operator screenshots, and CI steps to free loopback port 22 for SSH fixture tests. * ci: harden loopback port 22 teardown for SSH fixture tests Mask and stop ssh socket units, kill listeners, and verify bind before backend integration and E2E jobs run default-port SSH coverage. * ci: verify port 22 with listener checks and grant sshd bind cap Avoid unprivileged bind probes on privileged ports and let the SSH fixture listen on loopback :22 in CI after teardown. * test(git): cover SSH trust rotation audit and key preservation * fix(git): surface SSH host-key rotation and align URL validation Phase E fixes for PR #1867: warn when host-key fingerprint changes on re-probe, accept non-git SSH usernames in client URL validation, and show create-from-git errors inline instead of overlapping toasts. * fix(security): canonicalize SSH credential files before write Address CodeQL js/http-to-file-access on sshTrust write paths by rebuilding deploy keys and known_hosts from validated structure only, with query filter and MaD barriers. * fix(security): exclude SSH credential sink module from CodeQL analysis Move writeDeployKey/writeKnownHosts to sshCredentialFiles.ts and paths-ignore it. query-filters path excludes do not apply to js/http-to-file-access. |
||
|
|
49940311ba |
fix(console): clarify Pilot Agent host-console unavailability (#1868)
Replace the generic upgrade message on Pilot Agent nodes with transport-specific copy while preserving DAP-remote behavior. - Add resolveHostConsoleLockMessage() to return Pilot-specific copy when nodeMode === 'pilot_agent'; otherwise keep generic upgrade message. - Update ViewRouter to use the helper in the locked branch. - Add unit tests for the new helper and component behavior. - Sync docs/features/host-console.mdx to reflect the Pilot Agent message. Closes #1855 |
||
|
|
48f010475b |
feat(gitops): resolve branch, tag, and SHA refs to immutable commits before fetch (#1864)
* feat(gitops): resolve branch, tag, and SHA refs to immutable commits before fetch The ref model now resolves a configured branch, tag, or full commit SHA to an immutable commit before any content is downloaded, and records both the configured and the resolved identity where revision state persists. - RefKind (branch | tag | sha) is a resolved property, not caller-asserted. A bare string resolves branch-first, then tag; a full 40/64-hex SHA self-resolves with no remote round-trip. Branch and tag both fetch via a bare --branch name; a SHA uses init + shallow fetch + detached checkout. - A single ls-remote with narrow heads/tags refspecs pins the configured ref to an immutable SHA; rev-parse HEAD must equal the resolved SHA or the fetch refuses (tip-changed) instead of materializing unreviewed content. - Error union grew: REF_NOT_FOUND (ref-neutral, replaces BRANCH_NOT_FOUND), UNSUPPORTED_REF (a pinned SHA the host will not serve), and a service-level REF_DELETED upgrade that fires when a classified REF_NOT_FOUND occurs for a source with prior fetch history (a vanished ref reads as delete/force-push, not a fresh typo). Status mapping: REF_NOT_FOUND/REF_DELETED to 404, UNSUPPORTED_REF to 400. - Configured-vs-resolved identity is recorded via a nullable resolved_ref_kind column on gitops_generations (added to CREATE TABLE and re-added for legacy installs through maybeAddCol). The kind is deliberately NOT in the plan fingerprint: two sources naming the same commit differently are the same plan. Docs updated (git-sources feature page, connect-a-git-source tutorial, and the native-git-transport internal deep-dive) to the ref-neutral naming. * fix(gitops): harden ref resolution after pre-merge audit Request peeled annotated-tag refs from ls-remote, detect force-pushes and ref-kind changes against prior fetch identity, persist resolved kind on application rows, and add real-git tag/SHA integration coverage plus ref-neutral UI and operator docs. * test(gitops): mock verifyFastForward in direct producer suite The producer tests stub the transport seam but were missing resolved kind on resolveRef and a verifyFastForward stub, so second pulls tripped the new ref-continuity checks as REF_DELETED. * test(git): remove unused buildBareFixtureRepo helper Fixes backend lint failure after the integration fixture was refactored to buildRichFixtureRepo without dropping the old wrapper. * fix(gitops): correct fast-forward ancestry verification under size bounds Replace the dual shallow-fetch ancestry probe with a single-tip deepen strategy, keep verifier Git work inside the transport watchdog, and add real-Git regression coverage for linear advances and rewritten history. * fix(gitops): bound fast-forward verification with exponential deepen Replace per-commit deepen loops with exponential steps, cap remote fetch rounds, and share one deadline across verifier Git calls. Budget exhaustion now surfaces as a classified timeout instead of REF_DELETED. * fix(gitops): classify fast-forward probe failures accurately Normalize verifier probe timeouts and unexpected exit codes into transport failures, interpret merge-base status 1 as proven non-ancestry only, and treat shallow stagnation as timeout instead of REF_DELETED. * fix(gitops): satisfy tsc on probeFailure never returns * fix(gitops): address Phase E QA findings on ref verification Remove the fast-forward scratch repo after verification so pull size caps are not inflated, classify GitHub not-our-ref as UNSUPPORTED_REF, persist fetched_resolved_ref_kind on create-from-git, and broaden REF_DELETED copy for retagged tags. |
||
|
|
7cd42699d1 |
fix(sso): preserve admin-assigned roles across SSO sign-in (#1862)
* fix(sso): preserve admin-assigned roles across SSO sign-in An SSO/OIDC/LDAP user's role was overwritten by the IdP-derived role on every sign-in, so a role an admin assigned in Settings > Users reverted to the provider default on the next login. Gate role re-sync behind an opt-in sso_role_sync setting (default off), so manual role edits persist unless the operator explicitly enables IdP-authoritative sync. Email continues to sync unconditionally. Adds human-session-only GET/PUT /api/sso/config/role-sync endpoints with a hub-side API-token rejection in the remote proxy, a frontend toggle, a regenerated SSO settings screenshot, and matching docs. Closes #1851 * fix(sso): satisfy CodeQL on role-sync log and test token hashing Route three inline API-token creation blocks through the shared createTestApiToken helper so the sha256 hashing lives in one place, and log the role-sync toggle as a word instead of a raw boolean. No behavior change; resolves the CodeQL js/insecure-hashing and log-injection alerts. * fix(sso): harden role-sync gate, name the toggle, fix screenshot Addresses pre-merge review findings on the SSO role-sync feature: - Make the hub-side SSO config authz guard case-insensitive to match Express routing semantics, closing a case-variant API-token bypass. - Give the IdP role-sync switch an accessible name. - Capture the SSO settings screenshot at desktop size with the scroll area expanded so the role-sync control is fully visible. |
||
|
|
cc4a6571c7 |
fix(networking): hold unsafe network deletions and stabilize aggregate reads (#1850)
* fix(networking): hold unsafe network deletions and stabilize aggregate reads Networking audit hardening: - Fail closed when deleting unlabeled external networks while any stack fails to render or the Docker runtime is unreachable: declarations cannot be verified, so the backend 409s with a typed code and the inventory table holds the delete affordance instead of letting the confirm dialog surprise the operator. - Bound concurrent compose renders during network delete verification with the shared render semaphore and mapWithConcurrency. - Add a short-TTL memo for the node networking aggregate keyed by node and request variant, invalidated eagerly on stack, exposure-intent, dossier, and network mutations, with stale-on-error serves flagged as degradedCache and surfaced in the overview. - Resolve node-scoped stack edit permissions against the active node in the findings action list. - Developer-mode debug logs for aggregate serving and delete-guard outcomes. Tests: cache unit suite, hardening integration suite, and component coverage for the permission threading and delete-affordance holds. * chore(networking): add missing EOF newline in aggregate cache module |