1664 Commits
Author SHA1 Message Date
Juan Denis b2bba02c60 Merge pull request #92 from jhd3197/dev
Make install links survive the login screen, and speed up CI
v1.7.83
2026-08-06 01:50:27 -04:00
github-actions[bot] 20effa2a1b chore: bump version to 1.7.83 [skip ci] 2026-08-06 05:34:26 +00:00
Juan DenisandClaude Opus 5 ac48dc5b4e perf(ci): shard the backend suite across 4 runners
Backend CI was the entire pipeline wait. Measured on the last dev push:

    Extensions CI            12s
    Version Bump             18s
    Test System Utilities    35s
    Security Scan            41s
    Scripts CI               66s
    Release Build Smoke      68s
    Backend CI           21m 50s   <- everything else finished in a minute

Step timings put all of it in the test run itself (install deps 22s, ratchet
9s), so there is no caching win to take — it is 3173 tests against a
function-scoped `app` fixture that rebuilds the Flask app and all 80+ tables
per test. Fixing that fixture is the real repair and is planned separately;
this change buys the wall-clock back now without touching a single test.

Split over 4 runners via pytest-split (794/794/794/791). Sharding rather than
pytest-xdist is deliberate: each shard is its own VM, so the process-shared
state that makes in-process parallelism unsafe here (templates.json, APPS_DIR
— see the per-PID DB dance in tests/conftest.py) simply isn't shared, and no
test code has to change.

The ratchet moves to its own job because it must see the whole suite; a shard
only collects its quarter. It runs beside the shards and costs no wall-clock.

Two details:

- The shard command is scoped to `tests` rather than a bare `pytest`.
  Identical in CI (3173 either way, backend/dev-data/ being gitignored), but a
  bare pytest on a dev box tries to collect the locally deployed apps under
  backend/dev-data/ and dies during collection. The line is now copy-pasteable
  for debugging a red shard locally.
- With no .test_durations file, pytest-split balances by test count, not time,
  and these tests range from 0.1s to 10s+. If one shard becomes the new
  critical path, run once with --store-durations and commit the file.

main also drops out of `push` here: release.yml gates itself on this workflow
via ci-gate, so listing main ran the whole suite a second time on every merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 01:18:26 -04:00
Juan DenisandClaude Opus 5 d06c382364 ci: run the frontend lint gate, which ran nowhere
`npm run lint` was defined in frontend/package.json and documented in
CLAUDE.md, but no workflow ever invoked it. Grepping the whole workflow dir for
`npm run lint`/`eslint`/`npm run build` returned nothing, so three project-
specific checkers chained behind eslint were silently unenforced:

    check-settings-index  every Settings tab has a search-index entry
    check-theme-tokens    the theme-token whitelist stays in 3-way sync
    check-html-sinks      every raw-HTML sink is sanitized or annotated (XSS)

Lint currently passes: 926 warnings, 0 errors, and all three checkers green —
so this is safe to add as a blocking check today. eslint exits 0 on warnings,
so the gate is on errors only; add --max-warnings to freeze the count if the
warning drift ever needs a ratchet of its own.

Lint only, no build step: the frontend is already compiled in CI by Release
Build Smoke Test, whose scripts/build-release.sh runs `npm ci && npm run
build`. backend/app/** is in the paths because check-html-sinks scans it too,
for `|safe`, `Markup(` and `render_template_string`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 01:17:56 -04:00
Juan DenisandClaude Opus 5 44f5a32867 ci: stop validating the same commit twice
These five workflows triggered on both `push: [dev, main]` and
`pull_request: [dev, main]`, so one change was validated up to three times: on
the dev push, again on the dev->main PR, and a third time on the post-merge
push to main.

Narrow them to `push: [dev]` + `pull_request: [main]`:

- `pull_request: [dev]` was dead config. Every PR this repo has had targets
  main (checked back through #77, dependabot's included), so it never produced
  a run.
- `push: [main]` was redundant for release-smoke: release.yml's build-release
  job runs that same scripts/build-release.sh for real, on the same commit,
  moments later.

Also drops test-system-utils' `unit-tests` job. It ran
`pytest tests/test_utils_system.py` (44 tests) that Backend CI's bare `pytest`
already collects — there is no pytest.ini, addopts or collect_ignore narrowing
collection. The distro matrix and the raw-subprocess audit stay; those are the
parts Backend CI genuinely cannot do.

Coverage of main is unchanged: a pull_request run tests the merge result, and
release.yml still gates itself on the full backend suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 01:17:20 -04:00
Juan DenisandClaude Opus 5 4ccfa7e8cf test: ratchet 3138 -> 3173
10 from the template repo/sync suite added alongside the DEFAULT_REPOS fix;
the rest accumulated since the last ratchet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 00:39:21 -04:00
Juan DenisandClaude Opus 5 8000faad0b harden(auth): close three gaps in the post-login redirect validator
Reasoning about a redirect validator is how they ship broken, so this was
driven by a probe that asks the URL parser directly: for ~50 candidate
strings, does `new URL(candidate, base)` leave the origin, and does
sanitizeRedirect agree? Zero leaks before or after -- the origin property was
never broken. What the probe did surface was three ways to be wrong that are
not origin escapes:

1. Dot segments were accepted. `/..//evil.com` normalizes to `//evil.com`,
   which stays same-origin only because it is resolved against a base;
   assigned to window.location.href it leaves the site. Rather than audit
   every present and future consumer of the stored value, the gadget is now
   refused outright -- plain and percent-encoded, in any position. A dot
   inside a segment name (/files/.env) is untouched.

2. Auth routes were matched as exact strings, so /login/ and, worse,
   /login/callback/<provider> got through -- the latter re-runs the SSO
   callback with no code and lands the user on an error page right after a
   successful login. Now a prefix match on a "/" boundary, case-insensitive,
   which still leaves /logins and /login-help as valid destinations.

3. No expiry. A destination parked early in a tab session fired on any later
   login in that tab: park an install link, abandon it, log in normally half
   an hour later, get taken somewhere you had forgotten about. Now stamped and
   good for 30 minutes -- long enough for a password manager, a TOTP prompt,
   or an SSO round trip that includes signing up at the provider.

Also a 2048-char cap, and consume now refuses a missing, non-numeric or
future timestamp rather than trusting it: rememberRedirect is the only writer,
so anything else did not come from this module.

Tests go 9 -> 19, and now cover the remember/consume round trip (single use,
expiry, tampered storage, storage unavailable) against a fake sessionStorage
so the whole module still runs under plain node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 00:38:27 -04:00
Juan DenisandClaude Opus 5 c3d70ed18b fix(templates): point the default repo at a host that exists, and verify what it sends
DEFAULT_REPOS pointed at raw.githubusercontent.com/serverkit/templates --
the org `serverkit` does not exist (the registry is jhd3197/serverkit-templates,
and its default branch is master, not main). So it has 404'd for its entire
existence: no panel has ever fetched a template remotely, and every /templates
page has been showing only the 118 bundled backend/templates/*.yaml.

Now points at https://serverkit.ai/templates, which proxies the registry and
was built for this consumer -- it serves <repo_url>/index.json and the
<repo_url>/templates/<id>.yaml path this class derives, behind a TTL cache
with last-good fallback. Using the product domain also means a branch rename
upstream cannot silently empty every panel's catalog, which is the exact
failure this is fixing.

Fixing the default alone would only help fresh installs, because get_config
returns whatever a saved templates.json holds. Known-dead URLs are therefore
healed on read. Nothing is lost by rewriting a URL that never once resolved.

The checksum half is here rather than in a follow-up because this commit
ACTIVATES a download path that has never run: sync_templates wrote whatever
came back straight to disk, ignoring the sha256 the index pins for every one
of the 106 official entries. Turning on an unverified fetch days after the
panel learned to verify ed25519 signatures on extensions would be shipping a
known downgrade. Mismatch is now a hard refusal that never reaches disk;
absent hash is allowed and counted, mirroring unsigned-vs-invalid for
extensions. Content is written as bytes so the file on disk is exactly what
was hashed.

Note get_template's in-memory remote read (it parses without saving) is not
covered -- it has no index entry on hand and would need an extra fetch.

DEPLOY ORDER: serverkit.ai must ship the master-branch fix before this helps.
Until then the proxy 502s and sync finds nothing, same as today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 00:33:46 -04:00
Juan DenisandClaude Opus 5 6b4e7db242 feat(extensions): open an extension from /extensions?install=<slug>
The counterpart to Templates.jsx's ?install=<id>, which has worked for a
while; the extensions page never grew one, so serverkit.ai install links and
README badges had nowhere to land.

It opens the extension's detail modal rather than installing. A URL arriving
from another site must not be able to install anything on its own -- the
operator still presses Install, and the trust gates behind it (unreviewed,
unverified checksum, untrusted publisher key) all still fire exactly as they
do from a card click. An unknown slug says so instead of failing silently.

Deliberately reads the param on /extensions and not /marketplace: the old
path resolves through a <Navigate>, which drops the query string, so a link
to /marketplace?install=x would arrive with nothing to act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 00:16:16 -04:00
Juan DenisandClaude Opus 5 681ac1fa6b fix(auth): return to the intended page after login instead of the dashboard
PrivateRoute redirected to /login with no record of where the visitor was
going, and every post-auth path then navigated to '/'. Any deep link opened
without a session was silently discarded -- you landed on the dashboard with
no explanation of why the thing you clicked did not happen.

That was survivable when deep links were rare. It is not now: serverkit.ai
install links (/extensions?install=<slug>, /templates?install=<id>) arrive
from README badges, and by construction they are clicked by people who may
have no open session. Every first-time click hit this path.

The destination goes in sessionStorage rather than react-router location
state because the SSO flow leaves the origin entirely for the identity
provider and returns through /login/callback/<provider>; router state cannot
survive that. One mechanism covering all four post-auth exits (login link
redemption, password, 2FA, SSO) beats two each covering half.

sanitizeRedirect is the pure half and is validated on write AND on read.
Login is exactly where an open redirect hurts, so `//evil.com` and
`/\evil.com` -- both of which browsers read as protocol-relative -- are
rejected along with control characters and the auth routes themselves,
which would loop. Proving test: node --test
src/utils/__tests__/redirectAfterLogin.test.mjs (9 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 00:16:03 -04:00
Juan Denis a61ae889fd Merge pull request #91 from jhd3197/dev
Sign extension releases and take WordPress out of the core tree
v1.7.82
2026-08-05 23:20:45 -04:00
github-actions[bot] 7e20371f5c chore: bump version to 1.7.82 [skip ci] 2026-08-06 02:54:34 +00:00
Juan DenisandClaude Opus 5 8e55f2355e fix(install): unbreak curl | bash — guard BASH_SOURCE under set -u
Piped execution leaves BASH_SOURCE unset, so the sourceability guard's bare
${BASH_SOURCE[0]} was an unbound variable under `set -u` and aborted the
install before main() ever ran:

    bash: line 2107: BASH_SOURCE[0]: unbound variable

This broke the one-liner README/DEPLOYMENT document, on every distro — not
just the Debian 13 it was reported on (reproduced on debian:13 bash 5.2.37,
ubuntu:22.04 bash 5.1.16, almalinux:9 bash 5.1.8). CI never saw it because
every job runs `bash install.sh` from a checkout, where BASH_SOURCE IS set.

Use the same non-empty-first guard scripts/install.sh already carries, and
add the proving test the panel installer was missing (T41) — the agent
installer has had its twin since the same fix landed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:42:52 -04:00
Juan Denis e556baf238 test: ratchet 3117 -> 3138 (audit remediation proving tests) 2026-08-05 13:36:33 -04:00
Juan Denis 065b6ed61b docs: consent-gate matrix + seam lifecycle in EXTENSIONS.md 2026-08-05 13:36:33 -04:00
Juan Denis 7c6b0f3958 security: hook seam lifecycle + registrant binding
Audit remediation (extension seams):
- disable/uninstall tear down the extension's backup kinds, event
  types, and template providers (unregister_capabilities by slug);
  enable re-registers (idempotent)
- wordpress_bridge fails closed unless the extension row is ACTIVE;
  orphaned-site db snapshot route returns clean 503
- provider registrations bound to their registrant: cross-slug
  replace/hijack rejected for backup kinds, event types, template
  providers
2026-08-05 13:36:33 -04:00
Juan Denis 670e9d8af7 security: signature lifecycle, update-path consent gates, download hardening
Audit remediation (install/signing surface):
- sig verdict reset on every install path (no stale verified badge)
- registry update route mirrors install consent gates (unreviewed 404,
  unsigned-first-party/untrusted 409 until acknowledged, bad sig 400);
  update offers carry installed_source_type; frontend risk dialog gains
  update branch
- first_party entries without verified pinned-key signature require
  acknowledge (community reviewed exempt, review-stamp-bound)
- downloads: https-only with per-hop redirect revalidation, private/
  link-local IP rejection, 100MB zip + 64KB sig caps, github token
  https-only
- extraction: declared+actual 250MB decompression cap with cleanup
- install/update audit logs record the signature verdict
2026-08-05 13:36:20 -04:00
Juan Denis d2d2d861cd test: ratchet 3213 -> 3117 (WP extension-owned suites moved out)
Collected 3142; floor keeps the usual ~25 headroom for
environment-dependent collection.
2026-08-05 02:38:54 -04:00
Juan Denis b159c7a5bd docs: WP extraction complete — core data seam, Tier C status 2026-08-05 02:38:41 -04:00
Juan Denis ba3bc685fe test: split WP suites — extension-owned move to serverkit-wordpress
10 extension-owned suites (surface, extraction, import-zip, php errors/
limits, pipelines alias, plugin library, promote, reports, ssh import)
move to the extension repo via the SERVERKIT_WORDPRESS_DIR loader.
Panel-core suites (bridge, hooks-absent, seeding, authz sweeps) updated
for WP-absent behavior; conftest seeds the manifest onto test plugin rows.
2026-08-05 02:38:41 -04:00
Juan Denis e6008afb38 refactor(wordpress): remove in-tree extension (extracted)
The full WordPress suite now lives at github.com/jhd3197/serverkit-wordpress
as a standalone runtime-ESM extension (backend blueprints/services, game
dashboard UI, tests, CI). Installed from the registry or offered during
onboarding from the bundled snapshot. WP data models stay core per D1
(documented core-data-seam).
2026-08-05 02:38:24 -04:00
Juan Denis e1a05384be refactor(wordpress): drop WP literals from sidebar presets + builtin manifest 2026-08-05 02:38:14 -04:00
Juan Denis b3eca19f66 chore(registry): bundled index lists serverkit-wordpress
Offline-first installs see the WP entry; sha256 refreshes from the
published asset at panel release time.
2026-08-05 02:38:13 -04:00
Juan Denis 798467baba feat(sdk): bump to 1.2.0 + PluginSlot/ErrorBoundary exports
Runtime-ESM extensions (starting with serverkit-wordpress) need slot
mounting and error containment from the host; additive, versioned in
lockstep frontend/backend.
2026-08-05 02:38:13 -04:00
Juan Denis 72eaba34f2 refactor(wordpress): decouple core from the in-tree extension
Bridge, blueprint registration, app/attach flows and builtin job
handlers no longer assume the WP builtin is present; WP install paths
gitignored like other extracted extensions. WP absent = feature absent
gracefully (plan 52 phase 5, D4).
2026-08-05 02:37:48 -04:00
Juan Denis 0f8d179692 test: ratchet 3197 -> 3213 (wp hook inversion matrix) 2026-08-05 01:18:42 -04:00
Juan Denis 964f8eee7d docs: hook registration seams + core data seam in EXTENSIONS.md 2026-08-05 01:18:33 -04:00
Juan Denis d61985e73b test: WP hook inversion matrix (present x absent)
16 tests covering boot registration, idempotency, backup provider
resolve/validate/missing-state, event catalog present/absent, template
provider dispatch, monitor binding gate, health-sweep no-op, analytics
ext-to-ext guards. Existing suites minimally updated for the provider
interfaces (171 WP/monitor + 316 backup/events/templates green).
2026-08-05 01:18:32 -04:00
Juan Denis 060e600ffa feat(monitors): gate wordpress_site_id binding on WP presence
Create/update validation fails loudly naming the missing extension
instead of orphaning monitors; health sweep no-ops with zero WP sites
(plan 52 phase 3 task 17). StatusComponent.wordpress_site_id stays core
per D1.
2026-08-05 01:18:23 -04:00
Juan Denis e0888ecbba feat(templates): provider hook for extension template kinds
The wordpress template kind + wordpress-external-db preflight move behind
a template-provider hook the extension supplies; the backend filters the
catalog so the WP card simply does not appear when WP is absent — no
frontend change needed (plan 52 phase 3 task 16).
2026-08-05 01:18:23 -04:00
Juan Denis b153f68c12 feat(events): registered event-type catalog seam
Extensions register event types at load; wordpress.* types now come from
the WP extension instead of a hardcoded core catalog (plan 52 phase 3
task 15). emit_wp stays core as a dumb emitter helper.
2026-08-05 01:18:22 -04:00
Juan Denis 8d106aa5a0 feat(backups): dynamic target-provider registry
VALID_TARGET_TYPES is now provider-driven; the wordpress_site branch
moves behind a provider the WP extension registers. ProtectionPanel /
RestoreDrawer read available types from the API; policies whose provider
is absent surface a clear missing-provider state instead of crashing
(plan 52 phase 3 task 13).
2026-08-05 01:18:11 -04:00
Juan Denis 57d82c8c83 feat(extensions): boot-time hook registration seam
Extensions can register providers into core engines at load (backup
targets, event types, template kinds) via a core_hooks module; idempotent,
process-lifetime like blueprint registration. WP extension supplies its
entries through builtin-extensions/serverkit-wordpress/backend/core_hooks.py
(plan 52 phase 3, D4).
2026-08-05 01:18:02 -04:00
Juan Denis ef88b765d7 docs: extraction audit reflects phase-4 frontend relocation 2026-08-05 00:48:21 -04:00
Juan Denis 6d53935130 refactor(wordpress): drop residual core nav/title wiring
App.jsx /wordpress* title fallbacks removed (manifest page_titles cover
the routes); sidebar preset hiddenItems no longer hardcode the wordpress
slug — nav sources from extension contributions.
2026-08-05 00:48:20 -04:00
Juan Denis 5d8160dc73 refactor(wordpress): relocate frontend into the extension
5 WordPress* pages + components/wordpress (40 files) + 3 _wordpress*.scss
partials move from core into builtin-extensions/serverkit-wordpress/
frontend/ (synced to frontend/src/plugins via the baked-glob pattern).
200 imports rewritten to serverkit-sdk; remaining @/ imports only for
genuinely host-internal modules. main.scss drops the WP partials — the
extension bundle imports its own styles. npm build + lint green.
2026-08-05 00:48:12 -04:00
Juan Denis c17975ea7a feat(sdk): export layout/feedback/form primitives for extension UIs
TabGroupLayout, Modal, ConfirmDialog, Spinner/LoadingState, EmptyState,
Skeleton(SkeletonBoundary), and the ui/* form kit (Button, Input, Select,
Switch, ...) as generic re-exports so an extension page can mirror core
page chrome without deep imports. Proven out by the WordPress frontend
relocation (plan 52 phase 4).
2026-08-05 00:48:01 -04:00
Juan Denis 07fcc82778 feat(marketplace): games category with gamepad icon
Unknown categories already derived/filtered gracefully; this adds
games to the canonical order and gives it a lucide Gamepad2 icon
ahead of serverkit-minecraft landing in the registry.
2026-08-05 00:25:39 -04:00
Juan Denis 73de373b80 test: move minecraft suites to the extension repo, adjust ratchet
The four minecraft test files now live in serverkit-minecraft/tests/
(panel-hosted via SERVERKIT_MINECRAFT_DIR loader). Ratchet floor
3280 -> 3197 (-83 minecraft tests; collected 3222, floor intact).
2026-08-05 00:09:19 -04:00
Juan Denis e280a446c1 refactor(extensions): extract serverkit-minecraft to its own repo
The Minecraft game-server extension now lives at
github.com/jhd3197/serverkit-minecraft as a standalone runtime-ESM
extension (analytics/gpu repo pattern). It is installed from the
registry like any first-party extension, no longer bundled in-tree.
2026-08-05 00:09:11 -04:00
Juan Denis a4329124dd chore(extensions): author signing tool + signing spec docs
scripts/sign-extension.mjs signs a bundle with an ed25519 author key;
EXTENSIONS.md documents the format, key model, and verdict behaviors.
Generated private keys live in gitignored scripts/keys/.
2026-08-04 23:34:34 -04:00
Juan Denis 0299d334af feat(extensions): install consent UX with signature badges
Consent card shows permissions, version gate, and signature state
(Signed/Unsigned/Unknown signer/Invalid) with honest copy; 'Install
anyway' destructive path for acknowledged risk; invalid = disabled.
2026-08-04 23:34:26 -04:00
Juan Denis 31697cda9e feat(extensions): enforce signature policy in install flow
Invalid signature hard-blocks (400); untrusted key requires explicit
acknowledge (409 backstop); registry index carries signature/publisher
fields; install pinned to previewed sha256 with re-verified signature.
2026-08-04 23:34:15 -04:00
Juan Denis af64f3628d feat(extensions): ed25519 signing service + publisher key registry
Signature verification over extension bundles with pinned publisher
public keys; verdicts: verified/unsigned/unknown_signer/invalid.
24 tests: round-trip, tamper, malformed, untrusted key, forge attempts.
2026-08-04 23:34:02 -04:00
Juan Denis 347a7f319c Merge pull request #89 from jhd3197/dev
Close IDOR and authorization gaps across the backend API
v1.7.81
2026-08-04 10:16:16 -04:00
github-actions[bot] 20c25938c8 chore: bump version to 1.7.81 [skip ci] 2026-08-04 13:11:21 +00:00
Juan Denis 00c66556b0 Bump cryptography to 50.0.0
Update the backend dependency pin from `cryptography==48.0.1` to `cryptography==50.0.0` in `requirements.txt` to keep encryption tooling current.
2026-08-04 09:10:56 -04:00
github-actions[bot] 459d46d6f6 chore: bump version to 1.7.80 [skip ci] 2026-08-04 13:01:12 +00:00
Juan Denis 939af8bc76 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-04 09:00:24 -04:00
Juan Denis d961bd23f0 fix(api): close IDOR/authz gaps across deployment jobs, vaults, queues, docker, databases
Proactive hardening from an internal audit of the same broken-authorization
class as GHSA-6w78-q5vm-rfmh (authentication checked, app/workspace-level
authorization skipped):

- deployment_jobs: job detail/logs/retry resolve the job and gate on
  can_access_app / can_operate_app (app-less jobs: requester or admin);
  global job listing is admin-only
- secrets_webhooks: every vault/secret/webhook-endpoint/delivery by-id
  route resolves to its workspace and 404-seals non-members; secret
  reveal and signing-secret regeneration require workspace owner/admin;
  dead local admin_required placeholder removed
- shared_resources: variable-group CRUD and attach/detach gate on the
  resolved scope (workspace role / can_edit_app / can_operate_app),
  closing deploy-time env injection into foreign apps
- queue_bus: message receive/complete/fail/list on system-owned groups
  are admin-only; user-owned groups restricted to their owner
- docker: raw container inspect (exposes env vars) and container logs
  are admin-only, matching Decision 7; per-app logs stay app-gated
- databases: raw SQL query endpoints, host-level metadata reads, and
  process lists are admin-only; per-app managed database flows unchanged

Adds 4 persona-matrix regression suites (74 tests) and bumps the
test-count ratchet.
2026-08-04 08:53:27 -04:00