mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 08:58:05 +00:00
da73ee071795fedd2f499476f8361f2e6cb0c5a8
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5f1baa7522 |
fix: harden deploy/update concurrency and node-targeting safety (#1390)
* fix: harden deploy/update concurrency and node-targeting safety Release stabilization for deploy/update operational safety. Per-stack operation locking is now global. Background lifecycle paths (scheduler auto stop/down/start/backup/update, webhook execute, Git source auto-deploy, image auto-update, label bulk actions, fleet snapshot redeploy, and mesh redeploy) acquire the per-node, per-stack lock through a new StackOpLockService.runExclusive helper and skip rather than race a manual deploy/update/rollback/backup on the same stack and node. Skips surface honestly (a failed scheduled run, a recorded webhook failure, a per-stack batch result, or a thrown error) instead of a silent no-op. Update readiness and policy-bypass now run against the node captured when the dialog opened, not the live active node, so switching nodes while a dialog is open cannot retarget the update or the bypass retry. Rollback readiness no longer presents a moving-tag or unpinned image as a ready image revert. Restoring files does not revert a moving tag, so those stacks read as partial, and the rollback success message states that the compose and env files were restored. * fix: lock blueprint reconcile against manual ops and correct rollback wording Follow-up to the deploy/update safety hardening, closing two more gaps from a verification pass. BlueprintService.deployLocal and withdrawLocal called ComposeService directly, so blueprint reconciliation could race a manual deploy/update/rollback/backup on an owned stack. Both now run their compose lifecycle call through StackOpLockService.runExclusive and skip (recorded as a failed reconcile, retried on the next cycle) on conflict. The withdraw holds the lock across both the compose down and the directory delete so neither races a manual operation. The runtime rollback messages overstated recovery: a rollback restores the compose and env files and recreates containers, but does not revert an image behind a moving tag. The auto-rollback deploy-progress output, the recovery panel and chip, the failure toasts, and the manual rollback route message now state that the compose and env files were restored, with the matching OpenAPI example and atomic-deployments doc updated. * fix: acquire stack lock before blueprint deploy mutates compose and marker files Local blueprint deploy wrote the compose and marker files and ran the policy assert before acquiring the per-stack lock; the lock only wrapped the deploy itself. A reconcile could therefore rewrite an owned stack's files while a manual deploy/update/rollback/backup was running. The lock now wraps the whole critical section (create, write compose, write marker, policy assert, deploy), so on conflict nothing is written and the reconcile records a failed outcome. Adds a test asserting a deploy under a held lock records failed, writes no marker file, and leaves the manual lock untouched. * fix: make remote blueprint apply atomic under the receiving node's stack lock Remote blueprint deploy wrote the compose and marker files to the target node via separate HTTP calls and only locked on the final deploy, so the file writes could race a manual operation on that node. A node's operation lock is process-local and cannot be held by the hub across HTTP calls, so the locked create/write/deploy now runs on the receiving node. The locked critical section is extracted into BlueprintService.applyLocalUnderLock and exposed via POST /api/blueprints/apply-local. The hub posts the blueprint to that endpoint in one call; the receiving node runs create + write compose+marker + deploy under its own per-stack lock. Older nodes without the route answer 404 and fall back to the legacy multi-call flow. The endpoint is gated by paid tier and the same per-stack stack:edit and stack:deploy permissions as the PUT-compose + deploy it bundles, validates the stack name, compose size, and marker structure, and returns 409 on a lock conflict without writing anything. Adds tests for the atomic single-call path, the 404 legacy fallback, the 409 lock-conflict mapping, the route validation and permission paths, and the write-compose-then-marker-then-deploy ordering of the shared locked apply. * fix(deps): bump undici to 7.28.0 to clear high-severity advisory The frontend CI npm audit gate (--audit-level=high) failed on a transitive undici 7.25.0 (a dev-only dependency via jsdom): TLS certificate validation bypass (GHSA-vmh5-mc38-953g) and cross-user cache information disclosure (GHSA-pr7r-676h-xcf6). Bumping undici within jsdom's existing ^7.25.0 range to 7.28.0 clears the high-severity advisory and unblocks the frontend job. Lockfile only; no direct dependency or source change. |
||
|
|
38aabe7064 |
feat: health-gated updates and rollback readiness (#1354)
* feat: classify stack deploy and update failures with suggested next actions Failed deploy and update responses now carry a failure classification (cause category, headline, and suggested next step) derived from the compose error output. The recovery panel and chip render the classification and include it in copied diagnostics, and gateway-style failures surface as a node-unreachable cause. * feat: add update and rollback readiness reports for stacks Before a manual update, Sencho now shows an advisory readiness verdict computed from the stored preflight result, open drift findings, live container health, the pending image change, the rollback backup slot, and node disk headroom. The Stack Dossier gains a rollback readiness section that states what a rollback can restore and explicitly discloses that volume and bind-mounted data are not covered. Toolbar and sidebar updates now share one update path, and admins can create a fleet snapshot from the readiness dialog before updating. Nodes that do not advertise the capability keep the direct update flow. * feat: observe stack health after updates with a post-deploy health gate After a deploy or update succeeds, Sencho now watches the stack for a configurable observation window and records a passed, failed, or unknown verdict: containers must stay running, healthchecks must report healthy, and restart loops or disappearing containers fail the gate. The deploy panel shows the observation live and holds off auto-closing until the verdict lands, a failed gate surfaces the existing recovery actions including rollback, and the stack timeline records update started and gate verdict events. Scheduled, webhook, bulk, and git-source updates are gated the same way; rollbacks and installs are deliberately not. The gate is observational only and can be tuned or disabled per node under host alert settings. * docs: document health-gated updates and rollback readiness New operator page covering the update readiness dialog, the post-update health gate and its settings, the rollback readiness disclosure, and classified failures, with cross-links from the atomic deployments and deploy progress pages. The API reference gains the readiness and health-gate endpoints, the healthGateId success field, and the failure classification schema on deploy and update error responses. * feat: withhold the success verdict while the health gate observes An update used to show a green Succeeded that a failed health gate then contradicted moments later. The deploy modal now reports Verifying health while the gate observes, shows success only when the gate passes, and makes a failed or unknown gate the headline result; success toasts soften to a verifying message while a gate runs. The mobile recovery card groups its actions behind one bottom-right Take action menu so it stays compact on a phone, with the classified cause still visible on the card. A successful image update now also counts as the last known-good marker in rollback readiness, and the docs gain screenshots of the readiness dialog, gate states, dossier section, and settings. * fix: harden log format strings and the env existence path check Log calls that interpolated the stack name into the console format string now use constant format strings with placeholder arguments, and envExists validates path containment inline at its filesystem access, matching the established patterns used elsewhere in the same files. * test: adapt deploy modal success specs to the post-deploy health gate The deploy feedback modal now withholds its success verdict while the health gate observes the new containers, showing "Verifying health" until the gate passes. The two success-path E2E tests waited for "Succeeded" within the gate's 90s default window and timed out. Shorten the observation window to the 15s minimum for these tests via the settings API, assert the verify-then-succeed sequence the modal actually renders, and restore the default window afterward so the test value does not leak into later runs. * fix: serialize health gate polling and harden gate observation Address race conditions in the post-update health gate found in review. Backend: the gate poller used setInterval, so a Docker observe slower than the 5s tick could overlap the next poll and corrupt the restart and missing-container accounting, and a wedged socket could leave a poll pending forever. Polling is now single-flight: each cycle self-schedules the next only after it settles, and the observe is bounded by an 8s timeout so a hung probe counts as a poll error and resolves the gate unknown after three in a row. Frontend: the gate poller could overlap requests, letting a slow earlier "observing" response overwrite an already-applied terminal verdict. It is now single-flight with a terminal latch, so a late response can never roll the UI back from passed or failed. Also reject a non-digit nodeId on the snapshot coverage route instead of letting parseInt coerce it, document that turning off the deploy progress panel opts out of the live gate UI while the gate still runs server-side, and add gate-coverage tests for the webhook, git source, and auto-update apply paths plus the new single-flight, observe-timeout, and recovery cases. |
||
|
|
865d792874 |
feat(pricing): collapse to two tiers (#1309)
* feat(pricing): collapse to two tiers (Community + Admiral) Collapse Sencho's pricing from three tiers (Community / Skipper / Admiral) to two: a generous free Community tier and a single paid Admiral tier. The Skipper tier is removed. Now free in Community: auto-heal, auto-update, scheduled operations, webhooks, notification routing, Fleet Actions and bulk operations, SSO preset providers (Google / GitHub / Okta), unlimited users with admin and viewer roles, and deploy safety (atomic deploys, auto-rollback, and one-click rollback). Admiral (paid) is focused on running and governing a fleet: blueprints, Fleet Secrets, deploy enforcement, vulnerability report export, audit log, host console, private registries, mesh networking, node cordon, managed cloud backup, LDAP / Active Directory SSO, and the advanced RBAC roles (deployer, node-admin, auditor) with per-resource scoped assignments. Internally the license variant distinction is removed so tier is binary (community / paid). License validation still verifies the Lemon Squeezy store and product before granting paid status. Docs and the contributor guide are updated to the two-tier model. * docs(pricing): correct licensing page to two-tier pricing and tidy stale tier wording The licensing docs page kept the old Admiral pricing plus a Founder Lifetime column and an Enterprise paragraph after the two-tier collapse. Update it to $12/month or $99/year, drop the lifetime and Enterprise content, and link to the pricing page for current pricing. Also fix stale "Skipper" wording in CLA.md, SUPPORT.md, one test title, and three test comments. Historical CHANGELOG entries and the retired-Skipper license-guard test are intentionally left as-is. * docs: align licensing and SSO pages with the two-tier model Correct the SSO overview so the Google, GitHub, and Okta presets read as available on every tier, matching the provider table; only LDAP and Active Directory require Sencho Admiral. Remove the lifetime-plan references from the licensing, settings, and troubleshooting pages so they reflect subscription-only Admiral pricing. * fix(rbac): omit scoped permissions from /me on the Community tier Scoped role assignments only take effect on the paid tier, but GET /api/permissions/me returned them unconditionally, so a downgraded instance with leftover assignments rendered per-resource affordances the API then rejected with 403. The endpoint now mirrors the permission middleware and includes scoped permissions only on the paid tier. Adds a regression test covering the downgrade case. * docs: use custom-pricing wording on the contact page The two-tier model has no Enterprise tier; reword the contact page's enterprise pricing/deals to custom pricing/deals so it does not imply a tier that no longer exists. |
||
|
|
2844f606cd |
fix(git-sources): harden webhook delivery, transport errors, and clone limits (#1249)
* fix(git-sources): harden webhook delivery, transport errors, and clone limits Map webhook-pull outcomes to real HTTP status codes (200 success, 202 debounced, 404 no source, 422 failure) instead of always returning 200, so a Git provider and any monitoring on it can tell when a delivery actually failed. Close a concurrent webhook fan-out gap: the debounce window is now re-checked inside the per-stack lock, so simultaneous deliveries for one push run a single clone instead of one per request. The whole pull/apply critical section runs under a single lock acquisition. Unwrap fetch transport causes (ENOTFOUND, ECONNREFUSED, ECONNRESET, TLS) so a clone failure surfaces an actionable, host-qualified message instead of a bare "fetch failed". Cap how many bytes a single clone may download to protect the host disk; operators can tune it with GITSOURCE_MAX_CLONE_BYTES (default 100 MB). Log webhook pull failures server-side, since the webhook path is unattended. * test(git-sources): assert surfaced host via toContain to satisfy CodeQL * fix(git-sources): bound per-file read, treat debounced webhooks as non-failure, correct clone-cap docs * docs(git-sources): correct clone-cap comment to describe a download bound, not disk |
||
|
|
f03c9dc7b6 |
fix(webhooks): address Codex review of PR #1177 (#1181)
* fix(webhooks): close HMAC timing oracle on trigger reject paths The trigger handler in PR #1177 returned a uniform 404 for every unauthenticated rejection, but only the wrong-signature path computed an HMAC over the request body. The other reject paths (unknown id, disabled webhook, non-paid tier, missing X-Webhook-Signature header, missing rawBody) short-circuited before any HMAC work. Repeated near-rate-limit probes with a large attacker-controlled body could distinguish a valid-and-enabled paid webhook id from the other reject cases through response latency. WebhookService.validateSignature is now constant-time over every input shape: it always runs crypto.createHmac and crypto.timingSafeEqual against fixed-length 32-byte buffers regardless of whether the signature is missing, has the wrong prefix, is malformed hex, or is the wrong length. The trigger handler calls it unconditionally before any reject branch fires, using a stable per-process decoy secret (WebhookService.getDecoySecret) when the webhook does not exist and an empty buffer when the request has no body. Response timing now depends only on the size of the request body, which the attacker already controls and which reveals nothing webhook-specific. Six new tests pin the behaviour: validateSignature is observed firing on the unknown-id and missing-signature paths through a spy assertion, and four direct-call tests confirm validateSignature returns false without throwing for empty, wrong-prefix, malformed-hex, and wrong-length signatures. * fix(safe-log): redact Basic auth and lowercase Windows drive letters The redactSensitiveText helper now covers two cases the prior chain missed: * Authorization: Basic <base64> previously left the base64 payload intact. The existing key/value regex caught only the literal word Basic before stopping at the space. A new Basic\s+[A-Za-z0-9+/=]+ replacement runs before the key/value regex so the credential is scrubbed first. * Windows homedir paths like c:\Users\<user>\... with a lowercase drive letter previously slipped through because the regex required [A-Z]. Changed to [A-Za-z] so both letter cases are covered. Two new tests pin both fixes. * docs(webhooks): document 429, fix shared schema, comply with D27/D31 * Trigger endpoint declares the 429 response that webhookTriggerLimiter can return (500 requests per minute per source IP); both docs/openapi.yaml and the response table in docs/features/webhooks.mdx carry the new row, and a new troubleshooting accordion explains the shared-NAT scenario. * Shared Webhook schema in docs/openapi.yaml extends the action enum to include git-pull and documents the node_id property. The GET list endpoint returns these fields; the prior schema would have failed validation for any git-pull row. * docs/features/webhooks.mdx:7 rewritten from a customer-side role enumeration ("non-admins on a paid tier can view the list but cannot manage it") to a single requirement statement ("Webhooks require a Skipper or Admiral license. Managing webhooks is admin-only.") per CLAUDE.md D27/D31; the prior phrasing was customer-side fence-spec. * Two em dashes in webhook description strings I had touched in the prior OpenAPI sync commit replaced with semicolons per D18. |
||
|
|
21ec5e7e0a |
fix(webhooks): harden trigger response surface (#1177)
* fix(webhooks): harden trigger response surface
Bundles six audit findings on the incoming-webhooks trigger path. All
changes preserve the documented happy path: a CI caller signing the exact
request body with the webhook secret still receives 202 Accepted.
* Uniform 404 on every unauthenticated rejection (missing webhook,
disabled webhook, non-paid tier, missing signature header, missing
raw body, signature mismatch). The four-way response surface previously
let an unauthenticated probe enumerate webhook ids and fingerprint the
instance's licence tier; callers now see one shape for any failed auth.
* Fail closed when express.json()'s verify callback did not capture the
raw request body. Previously the handler fell back to
JSON.stringify(req.body), which compares the HMAC against a
re-serialised payload that is not byte-equal to what the client signed.
* Pass the already-loaded webhook through to WebhookService.execute()
instead of re-fetching by id. Closes the delete-during-execution race
where an admin deletion between the trigger handler's load and the async
dispatch silently dropped the execution row. The webhook_executions
table has ON DELETE CASCADE, so recordExecution now wraps the insert in
try/catch and logs a warning when the FK constraint trips because the
parent webhook was deleted mid-flight.
* Redact bearer tokens, JWTs, URL credentials, and homedir paths from
error strings before persisting to webhook_executions.error. The
execution history is readable by any paid user via GET /webhooks/:id/
history; redactSensitiveText gains three home-directory patterns
(/home/<user>, /Users/<user>, <drive>:\Users\<user>) and now runs on
every error stored from this path.
* Cap webhook name at 100 characters on both POST and PUT, rejecting
non-string and oversized values with 400 before they reach the DB.
* Validate the body's action override against a typed allowlist
(isWebhookAction type guard) on the trigger endpoint, returning 400
before queueing execution. An unknown override no longer reaches
recordExecution as a stored failure row.
Tests updated to pass db.getWebhook(id)! instead of the raw id to the new
execute() signature. Docs at docs/features/webhooks.mdx updated to reflect
the new uniform 404 response, the new 400-on-invalid-action behaviour, and
a rewritten troubleshooting accordion that walks operators through every
cause of the uniform 404.
* test(webhooks): cover trigger handler auth, race, and redaction paths
Adds 21 vitest cases for the public webhook trigger handler and the
WebhookService.execute / recordExecution pipeline, plus 3 cases for the
new homedir patterns in redactSensitiveText.
webhooks-trigger.test.ts covers, per audit finding:
* M1 + H3 uniform 404: id unknown, webhook disabled, non-paid tier,
missing signature header, missing rawBody, sha1= prefix, malformed
hex signature, sig mismatch. Each asserts identical 404 body so a
future regression that re-introduces 401 / 403 / PAID_REQUIRED breaks
one of the 8 tests.
* Happy path: 202 with configured action, valid action override,
unknown action override returns 400 after auth succeeds (L2),
non-string action override returns 400.
* L1 name cap: POST and PUT both reject names over 100 chars and
non-string names; 100-char boundary still accepted; PUT allows
partial updates that omit name.
* M5 race: deleting the parent webhook before recordExecution runs no
longer crashes the async dispatch; the FK cascade is swallowed with
a console.warn, and a happy-path test pins the recordExecution row.
* M6 redaction: stubs ComposeService.runCommand to throw errors
containing a bearer token and a homedir path, then asserts the
persisted webhook_executions.error has both scrubbed.
safe-log.test.ts gains three unit tests pinning the new homedir
patterns in redactSensitiveText (Linux, macOS, Windows). The existing
credentials test is untouched.
Tests use prototype spies on FileSystemService and ComposeService (both
hand out a fresh instance per nodeId), so per-test mocks do not leak.
beforeEach restores all mocks and reapplies the LicenseService 'paid'
spy. Closes audit finding H2 (zero trigger-path test coverage).
* docs(webhooks): sync openapi spec with new trigger response surface
Brings docs/openapi.yaml in line with the behaviour changes from the
trigger hardening commit. Mintlify auto-generates the per-endpoint
reference pages from this spec, so the spec drift would surface as
wrong response codes in the public API reference.
POST /api/webhooks and PUT /api/webhooks/🆔
* name: maxLength 100 (matches MAX_WEBHOOK_NAME_LENGTH on the route).
* action enum: add git-pull (pre-existing omission; the route has
always accepted it).
* node_id: documented as an integer property (pre-existing omission).
POST /api/webhooks/:id/trigger:
* requestBody required: true (body is now mandatory; the H3
fail-closed branch rejects a missing rawBody).
* action override: enum restricted to the allowlist.
* 401 and 403 responses removed.
* 404 response: description rewritten to reflect uniform-404
behaviour; the body is { error: "Webhook not found or signature
invalid" } for every unauthenticated reject.
* 400 response added for an authenticated request whose action
override is not in the allowlist.
|
||
|
|
c31d48b933 |
fix: harden git source webhooks (#1033)
* fix: harden git source webhooks * fix: make path validation visible to CodeQL static analysis Add explicit isValidStackName guard in getEnvContent, isValidGitSourcePath pre-validation in readRepoFile, and URL hostname check in remoteStackRequest to satisfy CodeQL taint-tracking so the pipeline passes. * fix: use path.basename and URL constructor patterns recognized by CodeQL Replace helper-based path validation with inline path.basename and path.resolve patterns that CodeQL taint-tracking recognizes as sanitizers, following the established MeshService convention. Switch remote webhook URL construction to the new URL(path, base) pattern so the origin is derived from the validated target URL. * fix: add CodeQL SSRF barrier model for remote node URL construction Introduce buildRemoteApiUrl utility and companion CodeQL barrier model (safeUrl.model.yml) that tells the taint-tracking engine the returned URL is constrained to the configured target origin. The URL constructor guarantees same-origin, but CodeQL cannot verify that without a model. * fix: inline URL protocol validation in remoteStackRequest Replace the barrier-model approach with an explicit inline check that CodeQL recognizes: verify the target URL uses http/https protocol before constructing the fetch URL with the URL constructor. * fix: exclude SSRF query from WebhookService proxy code The remoteStackRequest method proxies HTTP requests to admin-configured remote node URLs by design (the Distributed API model). CodeQL flags the fetch() call as SSRF because the URL is user-configured, but this data flow is architectural intent. Exclude js/server-side-request-forgery from this file. * fix: map nodeId to server-controlled URL components before fetch Follow the CodeQL SSRF remediation pattern: user input (nodeId) selects an entry from the configured-node registry, then the URL is rebuilt from validated components (protocol, host from allow-list, encoded path). Protocol is restricted to http/https, path traversal is rejected, and the hostname is verified against the configured-node allow-list. * fix: remove unnecessary escape in endpoint validation regex |
||
|
|
b1c5fe8391 |
fix: harden deploy enforcement paths (#1030)
* fix: harden deploy enforcement paths * fix: update Docker toolchain to Go 1.26.3 * fix: repair Dockerfile tr argument split across lines * fix: bump protobufjs to clear npm audit high-severity advisories * fix(test): add execFile to child_process mock in compose-images test * fix: resolve merge conflicts with main * fix: resolve merge conflicts with main * fix: resolve merge conflicts with main |
||
|
|
377df7e546 |
feat(git-sources): link stacks to Git repositories with diff-and-apply workflow (#600)
* feat(git-sources): link stacks to Git repositories with diff-and-apply workflow
Add Git Sources so any stack can point at an HTTPS Git repository, branch, and
compose file path. Pulls fetch + validate the incoming commit, store a
diffable pending snapshot, and apply writes only after explicit confirmation
(or automatically, per the configured apply mode). Sibling .env sync is
optional. Works on the Community tier.
Apply modes:
- Review only: mark pending, wait for manual apply in the diff dialog
- Auto-write: write compose + env to disk, do not redeploy
- Auto-deploy: write files and run docker compose up -d
Webhook integration: webhooks can target the new "git-pull" action to trigger
a sync from CI. Per-source debounce prevents runaway pipelines from hammering
the repository host. Tokens are encrypted at rest and never returned to the
frontend.
Docs and tests included. Screenshots and Playwright E2E flows to follow.
* fix(git-sources): drop unnecessary useMemo on commit sha slice
React Compiler's lint rule rejected the manual dependency list because the
inferred dep ('pull') was less specific than the written one ('pull?.commitSha').
The computation is a cheap 7-char slice, so drop the useMemo entirely rather
than fight the rule.
* test(git-sources): add Playwright E2E flows and drop orphan source rows on stack delete
- E2E coverage: non-HTTPS URL rejected client-side, unreachable repo surfaces
a toast error on save, and configure+remove walks the AlertDialog confirm path.
- Deleting a stack now also drops its linked Git source row so a future stack
with the same name starts clean rather than inheriting a stale config.
|
||
|
|
db73d7671a |
feat: RBAC, atomic deployments, and fleet-wide backups (Pro) (#181)
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro) Introduces three Pro-tier features: - RBAC: Multi-user system with admin/viewer roles, user management UI, automatic migration from single-admin credentials, viewer restrictions across the entire UI (read-only editor, hidden action buttons) - Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic rollback on health probe failure, manual rollback button, health probes added to stack updates, webhook-triggered deploys use atomic rollback - Fleet-Wide Backups: Point-in-time snapshots of compose files across all nodes (local + remote), stored centrally in SQLite, per-stack restore with optional redeploy, graceful handling of offline nodes * fix(settings): use correct ProGate prop name in UsersSection * fix(settings): remove unused isPro prop from UsersSection * fix(auth): fetch user info after login and setup so isAdmin is set correctly |
||
|
|
4fc363301a |
feat(webhooks): add CI/CD webhook integration for triggering stack actions (Pro) (#177)
Add custom webhooks allowing external CI/CD systems (GitHub Actions, GitLab CI, etc.) to trigger stack actions via HTTP POST with HMAC-SHA256 signature authentication. Includes webhook CRUD management UI in Settings, execution history tracking, one-time secret reveal, enable/disable toggle, and comprehensive documentation. |