* 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.
* 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.
* feat(git): classify managed-file changes before apply
Pull now builds a fingerprint-bound plan of adds, modifies, deletes, and local conflicts. Apply refuses stale or blocked plans instead of overwriting live files, and promotion stays the only filesystem mutator.
* fix(git): contain stack-dir probes before filesystem access
The missing-stack and root-.env existence checks now resolve against the compose base and refuse paths that escape it before lstat or existsSync.
* fix(git): address managed-file change plan audit blockers
Wire build-context live inventory into the planner, reject special file nodes without readFile, fingerprint configured project env files, enrich plan metadata, and compute the create plan before promotion. Redact drift ledger service keys for managed-path conflicts and clear pending plan columns on revision reset.
* fix(git): unblock change-plan CI sinks and fifo test
Hash stack files through a contained open plus fstat on the same handle so CodeQL no longer flags the lstat/read race, and create fifo fixtures with mkfifo instead of mkfifoSync.
* fix(git): preserve unowned context files and align candidate validation
Inspect prior and candidate build contexts together, delete only owned paths, reject context-root symlinks before walking, and validate with the env-file model deploy will use after promotion.
* fix(git): contain live context and candidate env path sinks
Inline resolve and startsWith at the lstat and access calls so containment is checked at the filesystem sink.
* fix(git): resolve live context walks from the compose root
Rebuild readdir, lstat, and access paths from the compose directory at each sink so containment is checked against a known-safe base.
* fix(git): validate synced env removal against post-promotion files
A managed .env that the next revision omits must not be used for candidate validation or invocation, because promotion deletes it. Context walks now bound directory entries and skip descendants under nested symlinks. Plan fingerprints bind review metadata and secret-path matching covers .env.* names.
* docs(git): capture classified change-plan review screenshots
Replace the old Monaco pull-preview images with the classified operation list used by Apply.
* fix(git): treat invocation drift as reviewable, not a file conflict
A live Compose command-line change is not a managed-file conflict. Reviewed apply records the incoming invocation; webhook auto-apply still refuses.
The release-only `update-screenshots` job opened a `chore/refresh-screenshots`
PR and immediately tried to squash-merge it. Branch protection (1 review,
6 status checks) rejected the merge on every release, leaving an open PR
behind. Screenshots will instead be refreshed manually after UI changes.
Removed:
- The `update-screenshots` job from ci.yml (~57 lines).
- The `paths-ignore: docs/images/**` push trigger filter; its sole purpose
was to break the auto-merge re-trigger cascade. Its absence also fixes a
latent bug where docs-only pushes to main would have skipped sync-docs.
- Four `head_ref != 'chore/refresh-screenshots'` guards in other jobs.
- The "doc screenshots" mention in the skip-bot-PRs comment.
Reworked the screenshot capture spec to be opt-in:
- playwright.config.ts now defines two projects. The default `chromium`
project ignores screenshots.spec.ts; a separate `screenshots` project
matches it and is invoked manually.
- The e2e CI job runs `--project=chromium` so the screenshots project
cannot accidentally run in CI.
- Updated the spec's module comment with the new manual invocation.
Net: 86 lines removed, 27 added.
* 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
* feat(pricing): revise pricing strategy and enforce variant-based seat limits
Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.
* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration
Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.
* fix(licensing): exempt Lemon Squeezy webhook from auth middleware
The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.
* feat(pricing): update pricing to final live rates
Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.
* refactor(licensing): remove server-side checkout/webhook for self-hosted model
Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.
- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow
* chore: normalize em-dashes to hyphens across codebase (linter)
* chore: remove accidentally tracked directories from index