From 51d997c6fbdd90f2096d22b435968eb29d870ac7 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 07:31:36 +0000
Subject: [PATCH 01/13] chore: Update current spec to outline Caddy 2.11.1
compatibility, security, and UX impact plan
---
docs/plans/current_spec.md | 589 +++++++++++++++++++++++++++++--------
1 file changed, 459 insertions(+), 130 deletions(-)
diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md
index ef2a4694..d47c1e29 100644
--- a/docs/plans/current_spec.md
+++ b/docs/plans/current_spec.md
@@ -1,194 +1,523 @@
---
-post_title: "Current Spec: Resolve Proxy Host Hostname Validation Test Failures"
+post_title: "Current Spec: Caddy 2.11.1 Compatibility, Security, and UX Impact Plan"
categories:
- actions
- - testing
+ - security
- backend
+ - frontend
+ - infrastructure
tags:
- - go
- - proxyhost
- - unit-tests
- - validation
-summary: "Focused plan to resolve failing TestProxyHostService_ValidateHostname malformed URL cases by aligning test expectations with intended validation behavior and validating via targeted service tests and coverage gate."
-post_date: 2026-02-22
+ - caddy
+ - xcaddy
+ - dependency-management
+ - vulnerability-management
+ - release-planning
+summary: "Comprehensive, phased plan to evaluate and safely adopt Caddy v2.11.1 in Charon, covering plugin compatibility, CVE impact, xcaddy patch retirement decisions, UI/UX exposure opportunities, and PR slicing strategy with strict validation gates."
+post_date: 2026-02-23
---
-## Active Plan: Resolve Failing Hostname Validation Tests
+## Active Plan: Caddy 2.11.1 Deep Compatibility and Security Rollout
-Date: 2026-02-22
+Date: 2026-02-23
Status: Active and authoritative
-Scope Type: Backend test-failure remediation (service validation drift analysis)
+Scope Type: Architecture/security/dependency research and implementation planning
Authority: This is the only active authoritative plan section in this file.
## Introduction
-This plan resolves backend run failures in `TestProxyHostService_ValidateHostname`
-for malformed URL cases while preserving intended hostname validation behavior.
+Charon’s control plane and data plane rely on Caddy as a core runtime backbone.
+Because Caddy is embedded and rebuilt via `xcaddy`, upgrading from
+`2.11.0-beta.2` to `2.11.1` is not a routine version bump: it impacts
+runtime behavior, plugin compatibility, vulnerability posture, and potential UX
+surface area.
-Primary objective:
+This plan defines a low-risk, high-observability rollout strategy that answers:
-- Restore green test execution in `backend/internal/services` with a minimal,
- low-risk change path.
+1. Which Caddy 2.11.x features should be exposed in Charon UI/API?
+2. Which existing Charon workarounds became redundant upstream?
+3. Which `xcaddy` dependency patches remain necessary vs removable?
+4. Which known vulnerabilities are fixed now and which should remain on watch?
## Research Findings
-### Evidence Collected
+### External release and security findings
+
+1. Official release statement confirms `v2.11.1` has no runtime code delta from
+ `v2.11.0` except CI/release process correction. Practical implication:
+ compatibility/security validation should target **2.11.x** behavior, not
+ 2.11.1-specific runtime changes.
+2. Caddy release lists six security patches (mapped to GitHub advisories):
+ - `CVE-2026-27590` → `GHSA-5r3v-vc8m-m96g` (FastCGI split_path confusion)
+ - `CVE-2026-27589` → `GHSA-879p-475x-rqh2` (admin API cross-origin no-cors)
+ - `CVE-2026-27588` → `GHSA-x76f-jf84-rqj8` (host matcher case bypass)
+ - `CVE-2026-27587` → `GHSA-g7pc-pc7g-h8jh` (path matcher escaped-case bypass)
+ - `CVE-2026-27586` → `GHSA-hffm-g8v7-wrv7` (mTLS client-auth fail-open)
+ - `CVE-2026-27585` → `GHSA-4xrr-hq4w-6vf4` (glob sanitization bypass)
+3. NVD/CVE.org entries are currently reserved/not fully enriched. GitHub
+ advisories are the most actionable source right now.
+
+### Charon architecture and integration findings
+
+1. Charon compiles custom Caddy in `Dockerfile` via `xcaddy` and injects:
+ - `github.com/greenpau/caddy-security`
+ - `github.com/corazawaf/coraza-caddy/v2`
+ - `github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0`
+ - `github.com/zhangjiayin/caddy-geoip2`
+ - `github.com/mholt/caddy-ratelimit`
+2. Charon applies explicit post-generation `go get` patching in `Dockerfile` for:
+ - `github.com/expr-lang/expr@v1.17.7`
+ - `github.com/hslatman/ipstore@v0.4.0`
+ - `github.com/slackhq/nebula@v1.9.7` (with comment indicating temporary pin)
+3. Charon CI has explicit dependency inspection gate in
+ `.github/workflows/docker-build.yml` to verify patched `expr-lang/expr`
+ versions in built binaries.
+
+### Plugin compatibility findings (highest risk area)
+
+Current plugin module declarations (upstream `go.mod`) target older Caddy cores:
+
+- `greenpau/caddy-security`: `caddy/v2 v2.10.2`
+- `hslatman/caddy-crowdsec-bouncer`: `caddy/v2 v2.10.2`
+- `corazawaf/coraza-caddy/v2`: `caddy/v2 v2.9.1`
+- `zhangjiayin/caddy-geoip2`: `caddy/v2 v2.10.0`
+- `mholt/caddy-ratelimit`: `caddy/v2 v2.8.0`
+
+Implication: compile success against 2.11.1 is plausible but not guaranteed.
+The plan must include matrix build/provision tests before merge.
+
+### Charon UX and config-surface findings
+
+Current Caddy-related UI/API exposure is narrow:
+
+- `frontend/src/pages/SystemSettings.tsx`
+ - state: `caddyAdminAPI`, `sslProvider`
+ - saves keys: `caddy.admin_api`, `caddy.ssl_provider`
+- `frontend/src/pages/ImportCaddy.tsx` and import components:
+ - Caddyfile parsing/import workflow, not runtime feature toggles
+- `frontend/src/api/import.ts`, `frontend/src/api/settings.ts`
+- Backend routes and handlers:
+ - `backend/internal/api/routes/routes.go`
+ - `backend/internal/api/handlers/settings_handler.go`
+ - `backend/internal/api/handlers/import_handler.go`
+ - `backend/internal/caddy/manager.go`
+ - `backend/internal/caddy/config.go`
+ - `backend/internal/caddy/types.go`
+
+No UI controls currently exist for new Caddy 2.11.x capabilities such as
+`keepalive_idle`, `keepalive_count`, `trusted_proxies_unix`,
+`renewal_window_ratio`, or `0-RTT` behavior.
-- Failing command output confirms two failing subtests:
- - `TestProxyHostService_ValidateHostname/malformed_https_URL`
- - `TestProxyHostService_ValidateHostname/malformed_http_URL`
-- Failure message for both cases: `invalid hostname format`.
+## Requirements (EARS)
-### Exact Files Involved
+1. WHEN evaluating Caddy `v2.11.1`, THE SYSTEM SHALL validate compatibility
+ against all currently enabled `xcaddy` plugins before changing production
+ defaults.
+2. WHEN security advisories in Caddy 2.11.x affect modules Charon may use,
+ THE SYSTEM SHALL document exploitability for Charon’s deployment model and
+ prioritize remediation accordingly.
+3. WHEN an `xcaddy` patch/workaround no longer provides value,
+ THE SYSTEM SHALL remove it only after reproducible build and runtime
+ validation gates pass.
+4. IF a Caddy 2.11.x feature maps to an existing Charon concept,
+ THEN THE SYSTEM SHALL prefer extending existing UI/components over adding new
+ parallel controls.
+5. WHEN no direct UX value exists, THE SYSTEM SHALL avoid adding UI for upstream
+ options and keep behavior backend-managed.
+6. WHEN this rollout completes, THE SYSTEM SHALL provide explicit upstream watch
+ criteria for unresolved/reserved CVEs and plugin dependency lag.
+
+## Technical Specifications
+
+### Compatibility scope map (code touch inventory)
+
+#### Build/packaging
-1. `backend/internal/services/proxyhost_service_validation_test.go`
- - Test function: `TestProxyHostService_ValidateHostname`
- - Failing cases currently expect `wantErr: false` for malformed URLs.
-2. `backend/internal/services/proxyhost_service.go`
- - Service function: `ValidateHostname(host string) error`
- - Behavior: strips scheme, then validates hostname characters; malformed
- residual values containing `:` are rejected with `invalid hostname format`.
+- `Dockerfile`
+ - `ARG CADDY_VERSION`
+ - `ARG XCADDY_VERSION`
+ - `caddy-builder` stage (`xcaddy build`, plugin list, `go get` patches)
+- `.github/workflows/docker-build.yml`
+ - binary dependency checks (`go version -m` extraction/gates)
+- `.github/renovate.json`
+ - regex managers tracking `Dockerfile` patch dependencies
+
+#### Caddy runtime config generation
-### Root Cause Determination
+- `backend/internal/caddy/manager.go`
+ - `NewManager(...)`
+ - `ApplyConfig(ctx)`
+- `backend/internal/caddy/config.go`
+ - `GenerateConfig(...)`
+- `backend/internal/caddy/types.go`
+ - JSON struct model for Caddy config (`Server`, `TrustedProxies`, etc.)
-- Root cause is **test expectation drift**, not runtime service regression.
-- `git blame` shows malformed URL test cases were added on 2026-02-22 with
- permissive expectations, while validation behavior rejecting malformed host
- strings predates those additions.
-- Existing behavior aligns with stricter hostname validation and should remain
- the default unless product requirements explicitly demand permissive handling
- of malformed host inputs.
+#### Settings and admin surface
-### Confidence Assessment
+- `backend/internal/api/handlers/settings_handler.go`
+ - `UpdateSetting(...)`, `PatchConfig(...)`
+- `backend/internal/api/routes/routes.go`
+ - Caddy manager wiring + settings routes
+- `frontend/src/pages/SystemSettings.tsx`
+ - current Caddy-related controls
+
+#### Caddyfile import behavior
+
+- `backend/internal/api/handlers/import_handler.go`
+ - `RegisterRoutes(...)`, `Upload(...)`, `GetPreview(...)`
+- `backend/internal/caddy/importer.go`
+ - `NormalizeCaddyfile(...)`, `ParseCaddyfile(...)`, `ExtractHosts(...)`
+- `frontend/src/pages/ImportCaddy.tsx`
+ - import UX and warning handling
+
+### Feature impact assessment (2.11.x)
+
+#### Candidate features for potential Charon exposure
+
+1. Keepalive server options (`keepalive_idle`, `keepalive_count`)
+ - Candidate mapping: advanced per-host connection tuning
+ - Likely files: `backend/internal/caddy/types.go`,
+ `backend/internal/caddy/config.go`, host settings API + UI
+2. `trusted_proxies_unix`
+ - Candidate mapping: trusted local socket proxy chains
+ - Current `TrustedProxies` struct lacks explicit unix-socket trust fields
+3. Certificate lifecycle tunables (`renewal_window_ratio`, maintenance interval)
+ - Candidate mapping: advanced TLS policy controls
+ - Potentially belongs under system-level TLS settings, not per-host UI
+
+#### Features likely backend-only / no new UI by default
+
+1. Reverse-proxy automatic `Host` rewrite for TLS upstreams
+2. ECH key auto-rotation
+3. `SIGUSR1` reload fallback behavior
+4. Logging backend internals (`timberjack`, ordering fixes)
+
+Plan decision rule: expose only options that produce clear operator value and
+can be represented without adding UX complexity.
+
+### Security patch relevance matrix
+
+#### Advisory exploitability rubric and ownership
+
+Use the following deterministic rubric for each advisory before any promotion:
+
+| Field | Required Values | Rule |
+| --- | --- | --- |
+| Exploitability | `Affected` / `Not affected` / `Mitigated` | `Affected` means a reachable vulnerable path exists in Charon runtime; `Not affected` means required feature/path is not present; `Mitigated` means vulnerable path exists upstream but Charon deployment/runtime controls prevent exploitation. |
+| Evidence source | advisory + code/config/runtime proof | Must include at least one authoritative upstream source (GitHub advisory/Caddy release) and one Charon-local proof (config path, test, scan, or runtime verification). |
+| Owner | named role | Security owner for final disposition (`QA_Security` lead or delegated maintainer). |
+| Recheck cadence | `weekly` / `release-candidate` / `on-upstream-change` | Minimum cadence: weekly until CVE enrichment is complete and disposition is stable for two consecutive checks. |
+
+Promotion gate: every advisory must have all four fields populated and signed by
+owner in the PR evidence bundle.
+
+#### High-priority for Charon context
+
+1. `GHSA-879p-475x-rqh2` (admin API cross-origin no-cors)
+ - Charon binds admin API internally but still uses `0.0.0.0:2019` in
+ generated config. Must verify actual network isolation and container
+ exposure assumptions.
+2. `GHSA-hffm-g8v7-wrv7` (mTLS fail-open)
+ - Relevant if client-auth CA pools are configured anywhere in generated or
+ imported config paths.
+3. matcher bypass advisories (`GHSA-x76f-jf84-rqj8`, `GHSA-g7pc-pc7g-h8jh`)
+ - Potentially relevant to host/path-based access control routing in Caddy.
+
+#### Contextual/conditional relevance
+
+- `GHSA-5r3v-vc8m-m96g` (FastCGI split_path)
+ - Relevant only if FastCGI transport is in active use.
+- `GHSA-4xrr-hq4w-6vf4` (file matcher glob sanitization)
+ - Relevant when file matchers are used in route logic.
+
+### xcaddy patch retirement candidates
+
+#### Candidate to re-evaluate for removal
+
+- `go get github.com/slackhq/nebula@v1.9.7`
+ - Upstream Caddy has moved forward to `nebula v1.10.3` and references
+ security-related maintenance in the 2.11.x line.
+ - Existing Charon pin comment may be stale after upstream smallstep updates.
-- Confidence score: **95% (High)**
-- Rationale: direct reproduction, targeted file inspection, and blame history
- converge on expectation drift.
+#### Likely retain until proven redundant
-## Requirements (EARS)
+- `go get github.com/expr-lang/expr@v1.17.7`
+- `go get github.com/hslatman/ipstore@v0.4.0`
-- WHEN malformed `http://` or `https://` host strings are passed to
- `ValidateHostname`, THE SYSTEM SHALL return a validation error.
-- WHEN service validation behavior is intentionally strict, THE TESTS SHALL
- assert rejection for malformed URL residual host strings.
-- IF product intent is permissive for malformed inputs, THEN THE SYSTEM SHALL
- minimally relax parsing logic without weakening valid invalid-character checks.
-- WHEN changes are completed, THE SYSTEM SHALL pass targeted service tests and
- the backend coverage gate script.
+Retention/removal decision must be made using reproducible build + binary
+inspection evidence, not assumption.
-## Technical Specification
+#### Hard retirement gates (mandatory before removing any pin)
-### Minimal Fix Path (Preferred)
+Pin removal is blocked unless all gates pass:
-Preferred path: **test-only correction**.
+1. Binary module diff gate
+ - Produce before/after `go version -m` module diff for Caddy binary.
+ - No unexpected module major-version jumps outside approved advisory scope.
+2. Security regression gate
+ - No new HIGH/CRITICAL findings in CodeQL/Trivy/Grype compared to baseline.
+3. Reproducible build parity gate
+ - Two clean rebuilds produce equivalent module inventory and matching runtime
+ smoke results.
+4. Rollback proof gate (mandatory, with explicit `nebula` focus)
+ - Demonstrate one-command rollback to previous pin set, with successful
+ compile + runtime smoke set after rollback.
-1. Update malformed URL table entries in
- `backend/internal/services/proxyhost_service_validation_test.go`:
- - `malformed https URL` -> `wantErr: true`
- - `malformed http URL` -> `wantErr: true`
-2. Keep current service behavior in
- `backend/internal/services/proxyhost_service.go` unchanged.
-3. Optional test hardening (still test-only): assert error contains
- `invalid hostname format` for those two cases.
+Retirement decision for `nebula` cannot proceed without explicit rollback proof
+artifact attached to PR evidence.
-### Alternative Path (Only if Product Intent Differs)
+### Feature-to-control mapping (exposure decision matrix)
-Use only if maintainers explicitly confirm malformed URL inputs should pass:
+| Feature | Control surface | Expose vs backend-only rationale | Persistence path |
+| --- | --- | --- | --- |
+| `keepalive_idle`, `keepalive_count` | Existing advanced system settings (if approved) | Expose only if operators need deterministic upstream connection control; otherwise keep backend defaults to avoid UX bloat. | `frontend/src/pages/SystemSettings.tsx` → `frontend/src/api/settings.ts` → `backend/internal/api/handlers/settings_handler.go` → DB settings → `backend/internal/caddy/config.go` (`GenerateConfig`) |
+| `trusted_proxies_unix` | Backend-only default initially | Backend-only until proven demand for unix-socket trust tuning; avoid misconfiguration risk in general UI. | backend config model (`backend/internal/caddy/types.go`) + generated config path (`backend/internal/caddy/config.go`) |
+| `renewal_window_ratio`, cert maintenance interval | Backend-only policy | Keep backend-only unless operations requires explicit lifecycle tuning controls. | settings store (if introduced) → `settings_handler.go` → `GenerateConfig` |
+| Reverse-proxy Host rewrite / ECH rotation / reload fallback internals | Backend-only | Operational internals with low direct UI value; exposing would increase complexity without clear user benefit. | backend runtime defaults and generated Caddy config only |
-1. Apply minimal service correction in `ValidateHostname` to normalize malformed
- scheme inputs before character validation.
-2. Add or update tests to preserve strict rejection for truly invalid hostnames
- (e.g., `$`, `@`, `%`, `&`) so validation is not broadly weakened.
+## Implementation Plan
-Decision default for this plan: **Preferred path (test updates only)**.
+### Phase 1: Playwright and behavior baselining (mandatory first)
+
+Objective: capture stable pre-upgrade behavior and ensure UI/UX parity checks.
+
+1. Run targeted E2E suites covering Caddy-critical flows:
+ - `tests/tasks/import-caddyfile.spec.ts`
+ - `tests/security-enforcement/zzz-caddy-imports/*.spec.ts`
+ - system settings-related tests around Caddy admin API and SSL provider
+2. Capture baseline artifacts:
+ - Caddy import warning behavior
+ - security settings save/reload behavior
+ - admin API connectivity assumptions from test fixtures
+3. Produce a baseline report in `docs/reports/` for diffing in later phases.
+
+### Phase 2: Backend and build compatibility research implementation
+
+Objective: validate compile/runtime compatibility of Caddy 2.11.1 with current
+plugin set and patch set.
+
+1. Bump candidate in `Dockerfile`:
+ - `ARG CADDY_VERSION=2.11.1`
+2. Execute matrix builds with toggles:
+ - Scenario A: current patch set unchanged
+ - Scenario B: remove `nebula` pin only
+ - Scenario C: remove `nebula` + retain `expr/ipstore`
+3. Execute explicit compatibility gate matrix (deterministic):
+
+ | Dimension | Values |
+ | --- | --- |
+ | Plugin set | `caddy-security`, `coraza-caddy`, `caddy-crowdsec-bouncer`, `caddy-geoip2`, `caddy-ratelimit` |
+ | Patch scenario | `A` current pins, `B` no `nebula` pin, `C` no `nebula` pin + retained `expr/ipstore` pins |
+ | Platform/arch | `linux/amd64`, `linux/arm64` |
+ | Runtime smoke set | boot Caddy, apply generated config, admin API health, import preview, one secured proxy request path |
+
+ Deterministic pass/fail rule:
+ - **Pass**: all plugin modules compile/load for the matrix cell AND all smoke
+ tests pass.
+ - **Fail**: any compile/load error, missing module, or smoke failure.
+
+ Promotion criteria:
+ - PR-1 promotion requires 100% pass for Scenario A on both architectures.
+ - Scenario B/C may progress only as candidate evidence; they cannot promote to
+ default unless all hard retirement gates pass.
+4. Validate generated binary dependencies from CI/local:
+ - verify `expr`, `ipstore`, `nebula`, `smallstep/certificates` versions
+5. Validate runtime config application path:
+ - `backend/internal/caddy/manager.go` → `ApplyConfig(ctx)`
+ - `backend/internal/caddy/config.go` → `GenerateConfig(...)`
+6. Run Caddy package tests and relevant integration tests:
+ - `backend/internal/caddy/*`
+ - security middleware integration paths that rely on Caddy behavior
+
+### Phase 3: Security hardening and vulnerability posture updates
+
+Objective: translate upstream advisories into Charon policy and tests.
+
+1. Add/adjust regression tests for advisory-sensitive behavior in
+ `backend/internal/caddy` and integration test suites, especially:
+ - host matcher behavior with large host lists
+ - escaped path matcher handling
+ - admin API cross-origin assumptions
+2. Update security documentation and operational guidance:
+ - identify which advisories are mitigated by upgrade alone
+ - identify deployment assumptions (e.g., local admin API exposure)
+3. Introduce watchlist process for RESERVED CVEs pending NVD enrichment:
+ - monitor Caddy advisories and module-level disclosures weekly
+
+### Phase 4: Frontend and API exposure decisions (only if justified)
+
+Objective: decide whether 2.11.x features merit UI controls.
+
+1. Evaluate additions to existing `SystemSettings` UX only (no new page):
+ - optional advanced toggles for keepalive tuning and trusted proxy unix scope
+2. Add backend settings keys and mapping only where persisted behavior is
+ needed:
+ - settings handler support in
+ `backend/internal/api/handlers/settings_handler.go`
+ - propagation to config generation in `GenerateConfig(...)`
+3. If no high-value operator need is proven, keep features backend-default and
+ document rationale.
+
+### Phase 5: Validation, docs, and release readiness
+
+Objective: ensure secure, reversible, and auditable rollout.
+
+1. Re-run full DoD sequence (E2E, patch report, security scans, coverage).
+2. Update architectural docs if behavior/config model changes.
+3. Publish release decision memo:
+ - accepted changes
+ - rejected/deferred UX features
+ - retained/removed patches with evidence
-## Implementation Plan
+## PR Slicing Strategy
-### Phase 1: Test-first Repro and Baseline
+### Decision
-1. Confirm current failure (already reproduced).
-2. Record failing subtests and error signatures as baseline evidence.
+Use **multiple PRs (PR-1/PR-2/PR-3)**.
-### Phase 2: Minimal Remediation
+Reasoning:
-1. Apply preferred test expectation update in
- `backend/internal/services/proxyhost_service_validation_test.go`.
-2. Keep service code unchanged unless product intent is clarified otherwise.
+1. Work spans infra/build security + backend runtime + potential frontend UX.
+2. Caddy is a blast-radius-critical dependency; rollback safety is mandatory.
+3. Review quality and CI signal are stronger with isolated, testable slices.
-### Phase 3: Targeted Validation
+### PR-1: Compatibility and evidence foundation
-Run in this order:
+Scope:
-1. `go test ./backend/internal/services -run TestProxyHostService_ValidateHostname -v`
-2. Related service package tests:
- - `go test ./backend/internal/services -run TestProxyHostService -v`
- - `go test ./backend/internal/services -v`
-3. Final gate:
- - `bash scripts/go-test-coverage.sh`
+- `Dockerfile` Caddy candidate bump (and temporary feature branch matrix toggles)
+- CI/workflow compatibility instrumentation if needed
+- compatibility report artifacts and plan-linked documentation
-## Risk Assessment
+Dependencies:
-### Key Risks
+- None
-1. **Semantic risk (low):** updating tests could mask an intended behavior
- change if malformed URL permissiveness was deliberate.
-2. **Coverage risk (low):** test expectation changes may alter branch coverage
- marginally but should not threaten gate based on current context.
-3. **Regression risk (low):** service runtime behavior remains unchanged in the
- preferred path.
+Acceptance criteria:
-### Mitigations
+1. Caddy 2.11.1 compiles with existing plugin set under at least one stable
+ patch scenario.
+2. Compatibility gate matrix (plugin × patch scenario × platform/arch × runtime
+ smoke set) executed with deterministic pass/fail output and attached evidence.
+3. Binary module inventory report generated and attached.
+4. No production behavior changes merged beyond compatibility scaffolding.
-- Keep change surgical to two table entries.
-- Preserve existing invalid-character rejection coverage.
-- Require full service package run plus coverage script before merge.
+Release guard (mandatory for PR-1):
-## Rollback Plan
+- Candidate tag only (`*-rc`/`*-candidate`) is allowed.
+- Release pipeline exclusion is required; PR-1 artifacts must not be eligible
+ for production release jobs.
+- Promotion to releasable tag is blocked until PR-2 security/retirement gates
+ pass.
-If maintainer/product decision confirms permissive malformed URL handling is
-required:
+Rollback notes:
-1. Revert the test expectation update commit.
-2. Implement minimal service normalization change in
- `backend/internal/services/proxyhost_service.go`.
-3. Add explicit tests documenting the accepted malformed-input behavior and
- retain strict negative tests for illegal hostname characters.
-4. Re-run targeted validation commands and coverage gate.
+- Revert `Dockerfile` arg changes and instrumentation only.
-## PR Slicing Strategy
+### PR-2: Security patch posture + patch retirement decision
-Decision: **Single PR**.
+Scope:
-Rationale:
+- finalize retained/removed `go get` patch lines in `Dockerfile`
+- update security tests/docs tied to six Caddy advisories
+- tighten/confirm admin API exposure assumptions
-- Scope is tightly bounded to one service test suite and one failure cluster.
-- Preferred remediation is test-only with low rollback complexity.
-- Review surface is small and dependency-free.
+Dependencies:
-Contingency split trigger:
+- PR-1 evidence
-- Only split if product intent forces service logic change, in which case:
- - PR-1: test expectation alignment rollback + service behavior decision record
- - PR-2: minimal service correction + adjusted tests
+Acceptance criteria:
-## Config/Infra File Impact Review
+1. Decision logged for each patch (`expr`, `ipstore`, `nebula`) with rationale.
+2. Advisory coverage matrix completed with Charon applicability labels.
+3. Security scans clean at required policy thresholds.
-Reviewed for required updates:
+Rollback notes:
-- `.gitignore`
-- `.dockerignore`
-- `codecov.yml`
-- `Dockerfile`
+- Revert patch retirement lines and keep previous pinned patch model.
+
+### PR-3: Optional UX/API exposure and cleanup
+
+Scope:
+
+- only approved high-value settings exposed in existing settings surface
+- backend mapping and frontend wiring using existing settings flows
+- docs and translations updates if UI text changes
+
+Dependencies:
+
+- PR-2 must establish stable runtime baseline first
+
+Acceptance criteria:
+
+1. No net-new page; updates land in existing `SystemSettings` domain.
+2. E2E and unit tests cover newly exposed controls and defaults.
+3. Deferred features explicitly documented with rationale.
+
+Rollback notes:
+
+- Revert UI/API additions while retaining already landed security/runtime upgrades.
+
+## Config File Review and Proposed Updates
+
+### Dockerfile (required updates)
+
+1. Update `ARG CADDY_VERSION` target to `2.11.1` after PR-1 gating.
+2. Reassess and potentially remove stale `nebula` pin in caddy-builder stage
+ if matrix build proves compatibility and security posture improves.
+3. Keep `expr`/`ipstore` patch enforcement until binary inspection proves
+ upstream transitive versions are consistently non-vulnerable.
+
+### .gitignore (suggested updates)
+
+No mandatory update for rollout, but recommended if new evidence artifacts are
+generated in temporary paths:
+
+- ensure transient compatibility artifacts are ignored (for example,
+ `test-results/caddy-compat/**` if used).
+
+### .dockerignore (suggested updates)
+
+No mandatory update; current file already excludes heavy test/docs/security
+artifacts and keeps build context lean. Revisit only if new compatibility
+fixture directories are introduced.
+
+### codecov.yml (suggested updates)
+
+No mandatory change for version upgrade itself. If new compatibility harness
+tests are intentionally non-coverage-bearing, add explicit ignore patterns to
+avoid noise in project and patch coverage reports.
+
+## Risk Register and Mitigations
-Planned changes: **None required** for this focused backend test-remediation
-scope.
+1. Plugin/API incompatibility with Caddy 2.11.1
+ - Mitigation: matrix compile + targeted runtime tests before merge.
+2. False confidence from scanner-only dependency policies
+ - Mitigation: combine advisory-context review with binary-level inspection.
+3. Behavioral drift in reverse proxy/matcher semantics
+ - Mitigation: baseline E2E + focused security regression tests.
+4. UI sprawl from exposing too many Caddy internals
+ - Mitigation: only extend existing settings surface when operator value is
+ clear and validated.
## Acceptance Criteria
-1. `TestProxyHostService_ValidateHostname` passes, including malformed URL
- subtests.
-2. `go test ./backend/internal/services -run TestProxyHostService -v` passes.
-3. `go test ./backend/internal/services -v` passes.
-4. `bash scripts/go-test-coverage.sh` passes final gate.
-5. Root cause is documented as expectation drift vs. service behavior drift, and
- chosen path is explicitly recorded.
+1. Charon builds and runs with Caddy 2.11.1 and current plugin set under
+ deterministic CI validation.
+2. A patch disposition table exists for `expr`, `ipstore`, and `nebula`
+ (retain/remove/replace + evidence).
+3. Caddy advisory applicability matrix is documented, including exploitability
+ notes for Charon deployment model.
+4. Any added settings are mapped end-to-end:
+ frontend state → API payload → persisted setting → `GenerateConfig(...)`.
+5. E2E, security scans, and coverage gates pass without regression.
+6. PR-1/PR-2/PR-3 deliverables are independently reviewable and rollback-safe.
+
+## Handoff
+
+After approval of this plan:
+
+1. Delegate PR-1 execution to implementation workflow.
+2. Require evidence artifacts before approving PR-2 scope reductions
+ (especially patch removals).
+3. Treat PR-3 as optional and value-driven, not mandatory for the security
+ update itself.
From 45458df1bfff264b238f743a6f9f06904f89f913 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 13:37:34 +0000
Subject: [PATCH 02/13] chore: Add Caddy compatibility gate workflow and
related scripts; enhance SMTP settings tests
---
.github/workflows/caddy-pr1-compat.yml | 57 +++
.github/workflows/release-goreleaser.yml | 12 +
.vscode/tasks.json | 7 +
Dockerfile | 33 +-
...al_test_pr1_caddy_compatibility_closure.md | 95 ++++
docs/plans/current_spec.md | 129 +++++
.../reports/caddy-pr1-compatibility-matrix.md | 33 ++
docs/reports/qa_report.md | 156 +-----
scripts/caddy-compat-matrix.sh | 464 ++++++++++++++++++
tests/core/proxy-hosts.spec.ts | 103 ++--
tests/settings/smtp-settings.spec.ts | 24 +-
11 files changed, 928 insertions(+), 185 deletions(-)
create mode 100644 .github/workflows/caddy-pr1-compat.yml
create mode 100644 docs/issues/manual_test_pr1_caddy_compatibility_closure.md
create mode 100644 docs/reports/caddy-pr1-compatibility-matrix.md
create mode 100755 scripts/caddy-compat-matrix.sh
diff --git a/.github/workflows/caddy-pr1-compat.yml b/.github/workflows/caddy-pr1-compat.yml
new file mode 100644
index 00000000..e5547292
--- /dev/null
+++ b/.github/workflows/caddy-pr1-compat.yml
@@ -0,0 +1,57 @@
+name: Caddy PR-1 Compatibility Gate
+
+on:
+ pull_request:
+ paths:
+ - Dockerfile
+ - scripts/caddy-compat-matrix.sh
+ - docs/plans/current_spec.md
+ - .github/workflows/caddy-pr1-compat.yml
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ compatibility-matrix:
+ name: PR-1 Compatibility Matrix (Candidate)
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+
+ - name: Set up Go
+ uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
+ with:
+ go-version: '1.26.0'
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+
+ - name: Run deterministic compatibility matrix gate
+ run: |
+ bash scripts/caddy-compat-matrix.sh \
+ --candidate-version 2.11.1 \
+ --patch-scenarios A,B,C \
+ --platforms linux/amd64,linux/arm64 \
+ --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health \
+ --output-dir test-results/caddy-compat \
+ --docs-report docs/reports/caddy-pr1-compatibility-matrix.md
+
+ - name: Upload compatibility artifacts
+ if: always()
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: caddy-pr1-compatibility-artifacts
+ path: |
+ test-results/caddy-compat/**
+ docs/reports/caddy-pr1-compatibility-matrix.md
+ retention-days: 14
diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml
index 0bab3e02..9846b125 100644
--- a/.github/workflows/release-goreleaser.yml
+++ b/.github/workflows/release-goreleaser.yml
@@ -20,6 +20,7 @@ permissions:
jobs:
goreleaser:
+ if: ${{ !contains(github.ref_name, '-candidate') && !contains(github.ref_name, '-rc') }}
runs-on: ubuntu-latest
env:
# Use the built-in GITHUB_TOKEN by default for GitHub API operations.
@@ -32,6 +33,17 @@ jobs:
with:
fetch-depth: 0
+ - name: Enforce PR-2 release promotion guard
+ env:
+ REPO_VARS_JSON: ${{ toJSON(vars) }}
+ run: |
+ PR2_GATE_STATUS="$(printf '%s' "$REPO_VARS_JSON" | jq -r '.CHARON_PR2_GATES_PASSED // "false"')"
+ if [[ "$PR2_GATE_STATUS" != "true" ]]; then
+ echo "::error::Releasable tag promotion is blocked until PR-2 security/retirement gates pass."
+ echo "::error::Set repository variable CHARON_PR2_GATES_PASSED=true only after PR-2 approval."
+ exit 1
+ fi
+
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c8eef9be..735cd618 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -724,6 +724,13 @@
"group": "test",
"problemMatcher": []
},
+ {
+ "label": "Security: Caddy PR-1 Compatibility Matrix",
+ "type": "shell",
+ "command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.1 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-pr1-compatibility-matrix.md",
+ "group": "test",
+ "problemMatcher": []
+ },
{
"label": "Test: E2E Playwright (Skill)",
"type": "shell",
diff --git a/Dockerfile b/Dockerfile
index fa421852..3f790457 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,6 +16,9 @@ ARG BUILD_DEBUG=0
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
## If the requested tag isn't available, fall back to a known-good v2.11.0-beta.2 build.
ARG CADDY_VERSION=2.11.0-beta.2
+ARG CADDY_CANDIDATE_VERSION=2.11.1
+ARG CADDY_USE_CANDIDATE=0
+ARG CADDY_PATCH_SCENARIO=A
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
@@ -196,6 +199,9 @@ FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION
+ARG CADDY_CANDIDATE_VERSION
+ARG CADDY_USE_CANDIDATE
+ARG CADDY_PATCH_SCENARIO
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
ARG XCADDY_VERSION=0.4.5
@@ -213,10 +219,16 @@ RUN --mount=type=cache,target=/go/pkg/mod \
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
sh -c 'set -e; \
+ CADDY_TARGET_VERSION="${CADDY_VERSION}"; \
+ if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \
+ CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \
+ fi; \
+ echo "Using Caddy target version: v${CADDY_TARGET_VERSION}"; \
+ echo "Using Caddy patch scenario: ${CADDY_PATCH_SCENARIO}"; \
export XCADDY_SKIP_CLEANUP=1; \
echo "Stage 1: Generate go.mod with xcaddy..."; \
# Run xcaddy to generate the build directory and go.mod
- GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
+ GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
--with github.com/greenpau/caddy-security \
--with github.com/corazawaf/coraza-caddy/v2 \
--with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \
@@ -239,12 +251,19 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
go get github.com/expr-lang/expr@v1.17.7; \
# renovate: datasource=go depName=github.com/hslatman/ipstore
go get github.com/hslatman/ipstore@v0.4.0; \
- # NOTE: smallstep/certificates (pulled by caddy-security stack) currently
- # uses legacy nebula APIs removed in nebula v1.10+, which causes compile
- # failures in authority/provisioner. Keep this pinned to a known-compatible
- # v1.9.x release until upstream stack supports nebula v1.10+.
- # renovate: datasource=go depName=github.com/slackhq/nebula
- go get github.com/slackhq/nebula@v1.9.7; \
+ if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
+ # NOTE: smallstep/certificates (pulled by caddy-security stack) currently
+ # uses legacy nebula APIs removed in nebula v1.10+, which causes compile
+ # failures in authority/provisioner. Keep this pinned to a known-compatible
+ # v1.9.x release until upstream stack supports nebula v1.10+.
+ # renovate: datasource=go depName=github.com/slackhq/nebula
+ go get github.com/slackhq/nebula@v1.9.7; \
+ elif [ "${CADDY_PATCH_SCENARIO}" = "B" ] || [ "${CADDY_PATCH_SCENARIO}" = "C" ]; then \
+ echo "Skipping nebula pin for scenario ${CADDY_PATCH_SCENARIO}"; \
+ else \
+ echo "Unsupported CADDY_PATCH_SCENARIO=${CADDY_PATCH_SCENARIO}"; \
+ exit 1; \
+ fi; \
# Clean up go.mod and ensure all dependencies are resolved
go mod tidy; \
echo "Dependencies patched successfully"; \
diff --git a/docs/issues/manual_test_pr1_caddy_compatibility_closure.md b/docs/issues/manual_test_pr1_caddy_compatibility_closure.md
new file mode 100644
index 00000000..ecb5ef02
--- /dev/null
+++ b/docs/issues/manual_test_pr1_caddy_compatibility_closure.md
@@ -0,0 +1,95 @@
+## Manual Test Tracking Plan — PR-1 Caddy Compatibility Closure
+
+- Date: 2026-02-23
+- Scope: PR-1 only
+- Goal: Track potential bugs in the completed PR-1 slice and confirm safe promotion.
+
+## In Scope Features
+
+1. Compatibility matrix execution and pass/fail outcomes
+2. Release guard behavior (promotion gate)
+3. Candidate build path behavior (`CADDY_USE_CANDIDATE=1`)
+4. Non-drift defaults (`CADDY_USE_CANDIDATE=0` remains default)
+
+## Out of Scope
+
+- PR-2 and later slices
+- Unrelated frontend feature behavior
+- Historical QA items not tied to PR-1
+
+## Environment Checklist
+
+- [ ] Local repository is up to date with PR-1 changes
+- [ ] Docker build completes successfully
+- [ ] Test output directory is clean or isolated for this run
+
+## Test Cases
+
+### TC-PR1-001 — Compatibility Matrix Completes
+
+- Area: Compatibility matrix
+- Risk: False PASS due to partial artifacts or mixed output paths
+- Steps:
+ 1. Run the matrix script with an isolated output directory.
+ 2. Verify all expected rows are present for scenarios A/B/C and amd64/arm64.
+ 3. Confirm each row has explicit PASS/FAIL values for required checks.
+- Expected:
+ - Matrix completes without missing rows.
+ - Row statuses are deterministic and readable.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR1-002 — Promotion Gate Enforces Scenario A Only
+
+- Area: Release guard
+- Risk: Incorrect gate logic blocks or allows promotion unexpectedly
+- Steps:
+ 1. Review matrix results for scenario A on amd64 and arm64.
+ 2. Confirm promotion decision uses scenario A on both architectures.
+ 3. Confirm scenario B/C are evidence-only and do not flip the promotion verdict.
+- Expected:
+ - Promotion gate follows PR-1 rule exactly.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR1-003 — Candidate Build Path Is Opt-In
+
+- Area: Candidate build path
+- Risk: Candidate path becomes active without explicit opt-in
+- Steps:
+ 1. Build with default arguments.
+ 2. Confirm runtime behavior is standard (non-candidate path).
+ 3. Build again with candidate opt-in enabled.
+ 4. Confirm candidate path is only active in the opt-in build.
+- Expected:
+ - Candidate behavior appears only when explicitly enabled.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR1-004 — Default Runtime Behavior Does Not Drift
+
+- Area: Non-drift defaults
+- Risk: Silent default drift after PR-1 merge
+- Steps:
+ 1. Verify Docker defaults used by standard build.
+ 2. Run a standard deployment path.
+ 3. Confirm behavior matches pre-PR-1 default expectations.
+- Expected:
+ - Default runtime remains non-candidate.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Defect Log
+
+Use this section for any issue found during manual testing.
+
+| ID | Test Case | Severity | Summary | Reproducible | Status |
+| --- | --- | --- | --- | --- | --- |
+| | | | | | |
+
+## Exit Criteria
+
+- [ ] All four PR-1 test cases executed
+- [ ] No unresolved critical defects
+- [ ] Promotion decision is traceable to matrix evidence
+- [ ] Any failures documented with clear next action
diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md
index d47c1e29..989da5b9 100644
--- a/docs/plans/current_spec.md
+++ b/docs/plans/current_spec.md
@@ -23,6 +23,135 @@ Status: Active and authoritative
Scope Type: Architecture/security/dependency research and implementation planning
Authority: This is the only active authoritative plan section in this file.
+## Focused Remediation Plan Addendum: 3 Failing Playwright Tests
+
+Date: 2026-02-23
+Scope: Only the 3 failures reported in `docs/reports/qa_report.md`:
+- `tests/core/proxy-hosts.spec.ts` — `should open edit modal with existing values`
+- `tests/core/proxy-hosts.spec.ts` — `should update forward host and port`
+- `tests/settings/smtp-settings.spec.ts` — `should update existing SMTP configuration`
+
+### Introduction
+
+This addendum defines a minimal, deterministic remediation for the three reported flaky/timeout E2E failures. The objective is to stabilize test synchronization and preconditions while preserving existing assertions and behavior intent.
+
+### Research Findings
+
+#### 1) `tests/core/proxy-hosts.spec.ts` (2 timeouts)
+
+Observed test pattern:
+- Uses broad selector `page.getByRole('button', { name: /edit/i }).first()`.
+- Uses conditional execution (`if (editCount > 0)`) with no explicit precondition that at least one editable row exists.
+- Waits for modal after clicking the first matched "Edit" button.
+
+Likely root causes:
+- Broad role/name selector can resolve to non-row or non-visible edit controls first, causing click auto-wait timeout.
+- Test data state is non-deterministic (no guaranteed editable proxy host before the update tests).
+- In-file parallel execution (`fullyParallel: true` globally) increases race potential for shared host list mutations.
+
+#### 2) `tests/settings/smtp-settings.spec.ts` (waitForResponse timeout)
+
+Observed test pattern:
+- Uses `clickAndWaitForResponse(page, saveButton, /\/api\/v1\/settings\/smtp/)`, which internally waits for response status `200` by default.
+- Test updates only host field, relying on pre-existing validity of other required fields.
+
+Likely root causes:
+- If backend returns non-`200` (e.g., `400` validation), helper waits indefinitely for `200` and times out instead of failing fast.
+- The test assumes existing SMTP state is valid; this is brittle under parallel execution and prior test mutations.
+
+### Technical Specifications (Exact Test Changes)
+
+#### A) `tests/core/proxy-hosts.spec.ts`
+
+1. In `test.describe('Update Proxy Host', ...)`, add serial mode:
+- Add `test.describe.configure({ mode: 'serial' })` at the top of that describe block.
+
+2. Add a local helper in this file for deterministic precondition and row-scoped edit action:
+- Helper name: `ensureEditableProxyHost(page, testData)`
+- Behavior:
+ - Check `tbody tr` count.
+ - If count is `0`, create one host via `testData.createProxyHost({ domain: ..., forwardHost: ..., forwardPort: ... })`.
+ - Reload `/proxy-hosts` and wait for content readiness using existing wait helpers.
+
+3. Replace broad edit-button lookup in both failing tests with row-scoped visible locator:
+- Replace:
+ - `page.getByRole('button', { name: /edit/i }).first()`
+- With:
+ - `const firstRow = page.locator('tbody tr').first()`
+ - `const editButton = firstRow.getByRole('button', { name: /edit proxy host|edit/i }).first()`
+ - `await expect(editButton).toBeVisible()`
+ - `await editButton.click()`
+
+4. Remove silent pass-through for missing rows in these two tests:
+- Replace `if (editCount > 0) { ... }` branching with deterministic precondition call and explicit assertion that dialog appears.
+
+Affected tests:
+- `should open edit modal with existing values`
+- `should update forward host and port`
+
+Preserved assertions:
+- Edit modal opens.
+- Existing values are present.
+- Forward host/port fields accept and retain edited values before cancel.
+
+#### B) `tests/settings/smtp-settings.spec.ts`
+
+1. In `test.describe('CRUD Operations', ...)`, add serial mode:
+- Add `test.describe.configure({ mode: 'serial' })` to avoid concurrent mutation of shared SMTP configuration.
+
+2. Strengthen required-field preconditions in failing test before save:
+- In `should update existing SMTP configuration`, explicitly set:
+ - `#smtp-host` to `updated-smtp.test.local`
+ - `#smtp-port` to `587`
+ - `#smtp-from` to `noreply@test.local`
+
+3. Replace status-constrained response wait that can timeout on non-200:
+- Replace `clickAndWaitForResponse(...)` call with `Promise.all([page.waitForResponse(...) , saveButton.click()])` matching URL + `POST` method (not status).
+- Immediately assert returned status is `200` and then keep success-toast assertion.
+
+4. Keep existing persistence verification and cleanup step:
+- Reload and assert host persisted.
+- Restore original host value after assertion.
+
+Preserved assertions:
+- Save request succeeds.
+- Success feedback shown.
+- Updated value persists after reload.
+- Original value restoration still performed.
+
+### Implementation Plan
+
+#### Phase 1 — Targeted test edits
+- Update only:
+ - `tests/core/proxy-hosts.spec.ts`
+ - `tests/settings/smtp-settings.spec.ts`
+
+#### Phase 2 — Focused verification
+- Run only the 3 failing cases first (grep-targeted).
+- Then run both files fully on Firefox to validate no local regressions.
+
+#### Phase 3 — Gate confirmation
+- Re-run the previously failing targeted suite:
+ - `tests/core`
+ - `tests/settings/smtp-settings.spec.ts`
+
+### Acceptance Criteria
+
+1. `should open edit modal with existing values` passes without timeout.
+2. `should update forward host and port` passes without timeout.
+3. `should update existing SMTP configuration` passes without `waitForResponse` timeout.
+4. No assertion scope is broadened; test intent remains unchanged.
+5. No non-target files are modified.
+
+### PR Slicing Strategy
+
+- Decision: **Single PR**.
+- Rationale: 3 deterministic test-only fixes, same domain (Playwright stabilization), low blast radius.
+- Slice:
+ - `PR-1`: Update the two spec files above + rerun targeted Playwright validations.
+- Rollback:
+ - Revert only spec-file changes if unintended side effects appear.
+
## Introduction
Charon’s control plane and data plane rely on Caddy as a core runtime backbone.
diff --git a/docs/reports/caddy-pr1-compatibility-matrix.md b/docs/reports/caddy-pr1-compatibility-matrix.md
new file mode 100644
index 00000000..42fde558
--- /dev/null
+++ b/docs/reports/caddy-pr1-compatibility-matrix.md
@@ -0,0 +1,33 @@
+## PR-1 Caddy Compatibility Matrix
+
+- Date: 2026-02-23
+- Candidate version: 2.11.1
+- Scope: PR-1 compatibility slice only
+
+## Promotion Rule (PR-1)
+
+- Promotion-gating rows: Scenario A on linux/amd64 and linux/arm64
+- Evidence-only rows: Scenario B and C
+
+## Matrix Summary
+
+| Scenario | Platform | Status | Reviewer Action |
+| --- | --- | --- | --- |
+| A | linux/amd64 | PASS | Required for promotion |
+| A | linux/arm64 | PASS | Required for promotion |
+| B | linux/amd64 | PASS | Evidence-only |
+| B | linux/arm64 | PASS | Evidence-only |
+| C | linux/amd64 | PASS | Evidence-only |
+| C | linux/arm64 | PASS | Evidence-only |
+
+## Decision
+
+- Promotion gate: PASS
+- Runtime default drift: None observed in PR-1
+- Candidate path: Opt-in only
+
+## Artifacts
+
+- Matrix CSV: test-results/caddy-compat-closure/matrix-summary.csv
+- Module inventories: test-results/caddy-compat-closure/module-inventory-*-go-version-m.txt
+- Module listings: test-results/caddy-compat-closure/module-inventory-*-modules.txt
diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md
index 9f5cdb21..766482d5 100644
--- a/docs/reports/qa_report.md
+++ b/docs/reports/qa_report.md
@@ -1,143 +1,31 @@
-## QA/Security Validation Report - Governance Documentation Slice
+## QA Report — PR-1 Caddy Compatibility Closure
-Date: 2026-02-20
-Repository: /projects/Charon
-Scope files:
-- `.github/instructions/copilot-instructions.md`
-- `.github/instructions/testing.instructions.md`
-- `.github/instructions/security-and-owasp.instructions.md`
-- `.github/agents/Management.agent.md`
-- `.github/agents/Backend_Dev.agent.md`
-- `.github/agents/QA_Security.agent.md`
-- `SECURITY.md`
-- `docs/security.md`
-- `docs/features/notifications.md`
+- Date: 2026-02-23
+- Scope: PR-1 compatibility slice only
+- Decision: Ready to close PR-1
-### Result Summary
+## Reviewer Checklist
-| Check | Status | Notes |
-|---|---|---|
-| 1) No secrets/tokens introduced in changed docs | PASS | No raw token values, API keys, or private credential material detected in scoped diffs; only policy/example strings were found. |
-| 2) Policy consistency verification | PASS | GORM conditional DoD gate, check-mode semantics, include/exclude trigger matrix, Gotify no-exposure + URL redaction, and precedence hierarchy are consistently present across canonical instructions and aligned agent/operator docs. |
-| 3) Markdown lint on scoped files | PASS | `markdownlint-cli2` reports baseline debt (`319` total), but intersection of lint hits with added hunk ranges for this governance slice returned no new lint hits in added sections. |
-| 4) Confirm governance-only scope for this slice | PASS | Scoped diff over the 9 target files confirms this implementation slice touches only those 9 governance files for evaluation. Unrelated branch changes were explicitly excluded by scope criteria. |
-| 5) QA report update for governance slice | PASS | This section added as the governance-slice QA record. |
+| Gate | Status | Reviewer Action |
+| --- | --- | --- |
+| Targeted Playwright blocker rerun | PASS | Confirm targeted tests are no longer failing. |
+| Compatibility matrix rerun (isolated output) | PASS | Confirm A/B/C rows exist for amd64 and arm64. |
+| Promotion guard decision | PASS | Confirm promotion depends only on Scenario A (both architectures). |
+| Non-drift runtime default | PASS | Confirm default remains non-candidate. |
+| Focused pre-commit and CodeQL findings gate | PASS | Confirm no blocking findings in this slice. |
-### Commands Executed
+## Evidence Snapshot
-```bash
-git diff --name-only -- .github/instructions/copilot-instructions.md .github/instructions/testing.instructions.md .github/instructions/security-and-owasp.instructions.md .github/agents/Management.agent.md .github/agents/Backend_Dev.agent.md .github/agents/QA_Security.agent.md SECURITY.md docs/security.md docs/features/notifications.md
+- Targeted rerun passed for prior blocker tests.
+- Matrix run completed with full rows and PASS outcomes in isolated output.
+- Promotion gate condition met: Scenario A passed on linux/amd64 and linux/arm64.
+- Candidate path remains opt-in; default path remains stable.
-git diff -U0 -- | grep '^+[^+]' | grep -Ei '(token|secret|api[_-]?key|password|ghp_|sk_|AKIA|xox|BEGIN)'
+## Open Risks to Monitor
-npx --yes markdownlint-cli2 \
- .github/instructions/copilot-instructions.md \
- .github/instructions/testing.instructions.md \
- .github/instructions/security-and-owasp.instructions.md \
- .github/agents/Management.agent.md \
- .github/agents/Backend_Dev.agent.md \
- .github/agents/QA_Security.agent.md \
- SECURITY.md docs/security.md docs/features/notifications.md
+- Matrix artifact contamination if shared output directories are reused.
+- Candidate behavior drift if default build args are changed in future slices.
-# Added-line lint intersection:
-# 1) build added hunk ranges from `git diff -U0 -- `
-# 2) run markdownlint output capture
-# 3) intersect (file,line) lint hits with added ranges
-# Result: no lint hits on added governance lines
-```
+## Final Verdict
-### Blockers
-
-- None specific to this governance slice.
-
-### Baseline Notes (Non-Blocking for This Slice)
-
-- Markdownlint baseline debt remains in the 9 scoped files and broader repository, but no new critical regression was introduced in governance-added sections for this slice.
-
-### Final Governance Slice Verdict
-
-**PASS** — All slice-scoped criteria passed under change-scope evaluation.
-
-## QA/Security Validation Report - PR-2 Frontend Slice
-
-Date: 2026-02-20
-Repository: /projects/Charon
-Scope: Final focused QA/security gate for notifications/security-event UX changes. Full E2E suite remains deferred to CI.
-
-### Gate Results
-
-| # | Required Check | Command(s) | Status | Evidence |
-|---|---|---|---|---|
-| 1 | Focused frontend tests for changed area | `cd frontend && npm run test -- src/pages/__tests__/Notifications.test.tsx src/pages/__tests__/Security.functional.test.tsx src/components/__tests__/SecurityNotificationSettingsModal.test.tsx src/api/__tests__/notifications.test.ts` | PASS | `4` files passed, `59` tests passed, `1` skipped. |
-| 2 | Frontend type-check | `cd frontend && npm run type-check` | PASS | `tsc --noEmit` completed with no errors. |
-| 3 | Frontend coverage gate | `.github/skills/scripts/skill-runner.sh test-frontend-coverage` | PASS | Coverage report: statements `87.86%`, lines `88.63%`; gate line threshold `85%` passed. |
-| 4 | Focused Playwright suite for notifications/security UX | `npx playwright test tests/settings/notifications.spec.ts --project=firefox`
`npx playwright test tests/security-enforcement/zzz-security-ui/system-security-settings.spec.ts --project=security-tests` | PASS | Notifications suite (prior run): `27/27` passed. Security settings focused suite (latest): `21/21` passed. |
-| 5 | Pre-commit fast hooks | `pre-commit run --files $(git diff --name-only --diff-filter=ACMRTUXB)` | PASS | Fast hooks passed, including `golangci-lint (Fast Linters - BLOCKING)`, `Go Vet`, `dockerfile validation`, `Frontend TypeScript Check`, and `Frontend Lint (Fix)`. |
-| 6 | CodeQL findings gate status (CI-aligned outputs) | Task `Security: CodeQL Go Scan (CI-Aligned) [~60s]`
Task `Security: CodeQL JS Scan (CI-Aligned) [~90s]`
`pre-commit run --hook-stage manual codeql-check-findings --all-files` | PASS | Fresh SARIF artifacts present (`codeql-results-go.sarif`, `codeql-results-js.sarif`); manual findings gate reports no HIGH/CRITICAL findings. |
-| 7 | Dockerized Trivy + Docker image scan status | `.github/skills/scripts/skill-runner.sh security-scan-trivy vuln,secret,misconfig json`
Task `Security: Scan Docker Image (Local)` | PASS | Existing Dockerized Trivy result remains passing from prior run. Latest local Docker image gate: `Critical: 0`, `High: 0` (effective gate pass). |
-
-### Confirmation of Prior Passing Gates (No Re-run)
-
-- Frontend tests/type-check/coverage remain confirmed PASS from prior validated run.
-- Pre-commit fast hooks remain confirmed PASS from prior validated run.
-- CodeQL Go + JS CI-aligned scans remain confirmed PASS from prior validated run.
-- Dockerized Trivy scan remains confirmed PASS from prior validated run.
-
-### Blocking Items
-
-- None for PR-2 focused QA/security scope.
-
-### Final Verdict
-
-- Overall Result: **PASS**
-- Full E2E regression remains deferred to CI as requested.
-- No remaining focused blockers identified.
-
-### Handoff References
-
-- Manual test plan (PR-1 + PR-2): `docs/issues/manual_test_provider_security_notifications_pr1_pr2.md`
-- Existing focused QA evidence in this report remains the baseline for automated validation.
-
-## QA/Security Validation Report - SMTP Flaky Test Fix (Test-Only Backend Change)
-
-Date: 2026-02-22
-Repository: /projects/Charon
-Scope: Validate SMTP STARTTLS test-stability fix without production behavior change.
-
-### Scope Verification
-
-| Check | Status | Evidence |
-|---|---|---|
-| Changed files are test-only (no production code changes) | PASS | `git status --short` shows only `backend/internal/services/mail_service_test.go` and `docs/plans/current_spec.md` modified. |
-| Production behavior unchanged by diff scope | PASS | No non-test backend/service implementation files modified. |
-
-### Required Validation Results
-
-| # | Command | Status | Evidence Snippet |
-|---|---|---|---|
-| 1 | `go test ./backend/internal/services -run TestMailService_TestConnection_StartTLSSuccessWithAuth -count=20` | PASS | `ok github.com/Wikid82/charon/backend/internal/services 1.403s` |
-| 2 | `go test -race ./backend/internal/services -run 'TestMailService_(TestConnection|Send)' -count=1` | PASS | `ok github.com/Wikid82/charon/backend/internal/services 1.270s` |
-| 3 | `bash scripts/go-test-coverage.sh` | PASS | `Statement coverage: 86.1%` / `Line coverage: 86.4%` / `Coverage requirement met` |
-| 4 | `pre-commit run --all-files` | PASS | All hooks passed, including `golangci-lint (Fast Linters - BLOCKING)`, `Go Vet`, `Frontend TypeScript Check`, `Frontend Lint (Fix)`. |
-
-### Additional QA Context
-
-| Check | Status | Evidence |
-|---|---|---|
-| Local patch coverage preflight artifacts generated | PASS | `bash scripts/local-patch-report.sh` produced `test-results/local-patch-report.md` and `test-results/local-patch-report.json`. |
-| Patch coverage threshold warning (advisory) | WARN (non-blocking) | Report output: `WARN: Overall patch coverage 53.8% ...` and `WARN: Backend patch coverage 52.0% ...`. |
-
-### Security Stance
-
-| Check | Status | Notes |
-|---|---|---|
-| New secret/token exposure risk introduced by test changes | PASS | Change scope is test helper logic only; no credentials/tokens were added to production paths, logs, or API outputs. |
-| Gotify token leakage pattern introduced | PASS | No Gotify tokenized URLs or token fields were added in the changed test file. |
-
-### Blockers
-
-- None.
-
-### Verdict
-
-**PASS** — SMTP flaky test fix validates as test-only, stable under repetition/race checks, meets backend coverage gate, passes full pre-commit, and introduces no new secret/token exposure risk.
+PR-1 closure gates are satisfied for the compatibility slice.
diff --git a/scripts/caddy-compat-matrix.sh b/scripts/caddy-compat-matrix.sh
new file mode 100755
index 00000000..fb2b1fe9
--- /dev/null
+++ b/scripts/caddy-compat-matrix.sh
@@ -0,0 +1,464 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+readonly DEFAULT_CANDIDATE_VERSION="2.11.1"
+readonly DEFAULT_PATCH_SCENARIOS="A,B,C"
+readonly DEFAULT_PLATFORMS="linux/amd64,linux/arm64"
+readonly DEFAULT_PLUGIN_SET="caddy-security,coraza-caddy,caddy-crowdsec-bouncer,caddy-geoip2,caddy-ratelimit"
+readonly DEFAULT_SMOKE_SET="boot_caddy,plugin_modules,config_validate,admin_api_health"
+
+OUTPUT_DIR="test-results/caddy-compat"
+DOCS_REPORT="docs/reports/caddy-pr1-compatibility-matrix.md"
+CANDIDATE_VERSION="$DEFAULT_CANDIDATE_VERSION"
+PATCH_SCENARIOS="$DEFAULT_PATCH_SCENARIOS"
+PLATFORMS="$DEFAULT_PLATFORMS"
+PLUGIN_SET="$DEFAULT_PLUGIN_SET"
+SMOKE_SET="$DEFAULT_SMOKE_SET"
+BASE_IMAGE_TAG="charon"
+KEEP_IMAGES="0"
+
+REQUIRED_MODULES=(
+ "http.handlers.auth_portal"
+ "http.handlers.waf"
+ "http.handlers.crowdsec"
+ "http.handlers.geoip2"
+ "http.handlers.rate_limit"
+)
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/caddy-compat-matrix.sh [options]
+
+Options:
+ --output-dir Output directory (default: test-results/caddy-compat)
+ --docs-report Markdown report path (default: docs/reports/caddy-pr1-compatibility-matrix.md)
+ --candidate-version Candidate Caddy version (default: 2.11.1)
+ --patch-scenarios Patch scenarios CSV (default: A,B,C)
+ --platforms Platforms CSV (default: linux/amd64,linux/arm64)
+ --plugin-set Plugin set descriptor for report metadata
+ --smoke-set Smoke set descriptor for report metadata
+ --base-image-tag Base image tag prefix (default: charon)
+ --keep-images Keep generated local images
+ -h, --help Show this help
+
+Deterministic pass/fail:
+ Promotion gate PASS only if Scenario A passes on linux/amd64 and linux/arm64.
+ Scenario B/C are evidence-only and do not fail the promotion gate.
+EOF
+}
+
+require_cmd() {
+ local cmd="$1"
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "ERROR: Required command not found: $cmd" >&2
+ exit 1
+ fi
+}
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --output-dir)
+ OUTPUT_DIR="$2"
+ shift 2
+ ;;
+ --docs-report)
+ DOCS_REPORT="$2"
+ shift 2
+ ;;
+ --candidate-version)
+ CANDIDATE_VERSION="$2"
+ shift 2
+ ;;
+ --patch-scenarios)
+ PATCH_SCENARIOS="$2"
+ shift 2
+ ;;
+ --platforms)
+ PLATFORMS="$2"
+ shift 2
+ ;;
+ --plugin-set)
+ PLUGIN_SET="$2"
+ shift 2
+ ;;
+ --smoke-set)
+ SMOKE_SET="$2"
+ shift 2
+ ;;
+ --base-image-tag)
+ BASE_IMAGE_TAG="$2"
+ shift 2
+ ;;
+ --keep-images)
+ KEEP_IMAGES="1"
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ usage
+ exit 1
+ ;;
+ esac
+ done
+}
+
+prepare_dirs() {
+ mkdir -p "$OUTPUT_DIR"
+ mkdir -p "$(dirname "$DOCS_REPORT")"
+}
+
+write_reports_header() {
+ local metadata_file="$OUTPUT_DIR/metadata.env"
+ local summary_csv="$OUTPUT_DIR/matrix-summary.csv"
+
+ cat > "$metadata_file" < "$summary_csv"
+}
+
+contains_value() {
+ local needle="$1"
+ shift
+ local value
+ for value in "$@"; do
+ if [[ "$value" == "$needle" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+enforce_required_gate_dimensions() {
+ local -n scenario_ref=$1
+ local -n platform_ref=$2
+
+ if ! contains_value "A" "${scenario_ref[@]}"; then
+ echo "[compat] ERROR: Scenario A is required for PR-1 promotion gate" >&2
+ return 1
+ fi
+
+ if ! contains_value "linux/amd64" "${platform_ref[@]}"; then
+ echo "[compat] ERROR: linux/amd64 is required for PR-1 promotion gate" >&2
+ return 1
+ fi
+
+ if ! contains_value "linux/arm64" "${platform_ref[@]}"; then
+ echo "[compat] ERROR: linux/arm64 is required for PR-1 promotion gate" >&2
+ return 1
+ fi
+}
+
+validate_matrix_completeness() {
+ local summary_csv="$1"
+ local -n scenario_ref=$2
+ local -n platform_ref=$3
+
+ local expected_rows
+ expected_rows=$(( ${#scenario_ref[@]} * ${#platform_ref[@]} ))
+
+ local actual_rows
+ actual_rows="$(tail -n +2 "$summary_csv" | sed '/^\s*$/d' | wc -l | tr -d '[:space:]')"
+
+ if [[ "$actual_rows" != "$expected_rows" ]]; then
+ echo "[compat] ERROR: matrix completeness failed (expected ${expected_rows} rows, found ${actual_rows})" >&2
+ return 1
+ fi
+
+ local scenario
+ local platform
+ for scenario in "${scenario_ref[@]}"; do
+ for platform in "${platform_ref[@]}"; do
+ if ! grep -q "^${scenario},${platform}," "$summary_csv"; then
+ echo "[compat] ERROR: missing matrix cell scenario=${scenario} platform=${platform}" >&2
+ return 1
+ fi
+ done
+ done
+}
+
+evaluate_promotion_gate() {
+ local summary_csv="$1"
+
+ local scenario_a_failures
+ scenario_a_failures="$(tail -n +2 "$summary_csv" | awk -F',' '$1=="A" && $10=="FAIL" {count++} END {print count+0}')"
+ local evidence_failures
+ evidence_failures="$(tail -n +2 "$summary_csv" | awk -F',' '$1!="A" && $10=="FAIL" {count++} END {print count+0}')"
+
+ if [[ "$evidence_failures" -gt 0 ]]; then
+ echo "[compat] Evidence-only failures (Scenario B/C): ${evidence_failures}"
+ fi
+
+ if [[ "$scenario_a_failures" -gt 0 ]]; then
+ echo "[compat] Promotion gate result: FAIL (Scenario A failures: ${scenario_a_failures})"
+ return 1
+ fi
+
+ echo "[compat] Promotion gate result: PASS (Scenario A on both required architectures)"
+}
+
+build_image_for_cell() {
+ local scenario="$1"
+ local platform="$2"
+ local image_tag="$3"
+
+ docker buildx build \
+ --platform "$platform" \
+ --load \
+ --pull \
+ --build-arg CADDY_USE_CANDIDATE=1 \
+ --build-arg CADDY_CANDIDATE_VERSION="$CANDIDATE_VERSION" \
+ --build-arg CADDY_PATCH_SCENARIO="$scenario" \
+ -t "$image_tag" \
+ . >/dev/null
+}
+
+smoke_boot_caddy() {
+ local image_tag="$1"
+ docker run --rm --pull=never --entrypoint caddy "$image_tag" version >/dev/null
+}
+
+smoke_plugin_modules() {
+ local image_tag="$1"
+ local output_file="$2"
+ docker run --rm --pull=never --entrypoint caddy "$image_tag" list-modules > "$output_file"
+
+ local module
+ for module in "${REQUIRED_MODULES[@]}"; do
+ grep -q "^${module}$" "$output_file"
+ done
+}
+
+smoke_config_validate() {
+ local image_tag="$1"
+ docker run --rm --pull=never --entrypoint sh "$image_tag" -lc '
+ cat > /tmp/compat-config.json <<"JSON"
+{
+ "admin": {"listen": ":2019"},
+ "apps": {
+ "http": {
+ "servers": {
+ "compat": {
+ "listen": [":2080"],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "body": "compat-ok",
+ "status_code": 200
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
+JSON
+ caddy validate --config /tmp/compat-config.json >/dev/null
+ '
+}
+
+smoke_admin_api_health() {
+ local image_tag="$1"
+ local admin_port="$2"
+ local run_id="compat-${admin_port}"
+
+ docker run -d --name "$run_id" --pull=never --entrypoint sh -p "${admin_port}:2019" "$image_tag" -lc '
+ cat > /tmp/admin-config.json <<"JSON"
+{
+ "admin": {"listen": ":2019"},
+ "apps": {
+ "http": {
+ "servers": {
+ "admin": {
+ "listen": [":2081"],
+ "routes": [
+ {
+ "handle": [
+ { "handler": "static_response", "body": "admin-ok", "status_code": 200 }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
+JSON
+ caddy run --config /tmp/admin-config.json
+ ' >/dev/null
+
+ local attempts=0
+ until curl -sS "http://127.0.0.1:${admin_port}/config/" >/dev/null 2>&1; do
+ attempts=$((attempts + 1))
+ if [[ $attempts -ge 30 ]]; then
+ docker logs "$run_id" || true
+ docker rm -f "$run_id" >/dev/null 2>&1 || true
+ return 1
+ fi
+ sleep 1
+ done
+
+ docker rm -f "$run_id" >/dev/null 2>&1 || true
+}
+
+extract_module_inventory() {
+ local image_tag="$1"
+ local output_prefix="$2"
+
+ local container_id
+ container_id="$(docker create --pull=never "$image_tag")"
+ docker cp "${container_id}:/usr/bin/caddy" "${output_prefix}-caddy"
+ docker rm "$container_id" >/dev/null
+
+ if command -v go >/dev/null 2>&1; then
+ go version -m "${output_prefix}-caddy" > "${output_prefix}-go-version-m.txt" || true
+ else
+ echo "go toolchain not available; module inventory skipped" > "${output_prefix}-go-version-m.txt"
+ fi
+
+ docker run --rm --pull=never --entrypoint caddy "$image_tag" list-modules > "${output_prefix}-modules.txt"
+}
+
+run_cell() {
+ local scenario="$1"
+ local platform="$2"
+ local cell_index="$3"
+ local summary_csv="$OUTPUT_DIR/matrix-summary.csv"
+ local safe_platform
+ safe_platform="${platform//\//-}"
+
+ local image_tag="${BASE_IMAGE_TAG}:caddy-${CANDIDATE_VERSION}-candidate-${scenario}-${safe_platform}"
+ local module_prefix="$OUTPUT_DIR/module-inventory-${scenario}-${safe_platform}"
+ local modules_list_file="$OUTPUT_DIR/modules-${scenario}-${safe_platform}.txt"
+ local admin_port=$((22019 + cell_index))
+ local checked_plugins
+ checked_plugins="${REQUIRED_MODULES[*]}"
+ checked_plugins="${checked_plugins// /;}"
+
+ echo "[compat] building cell scenario=${scenario} platform=${platform}"
+
+ local boot_status="FAIL"
+ local modules_status="FAIL"
+ local validate_status="FAIL"
+ local admin_status="FAIL"
+ local inventory_status="FAIL"
+ local cell_status="FAIL"
+
+ if build_image_for_cell "$scenario" "$platform" "$image_tag"; then
+ smoke_boot_caddy "$image_tag" && boot_status="PASS" || boot_status="FAIL"
+ smoke_plugin_modules "$image_tag" "$modules_list_file" && modules_status="PASS" || modules_status="FAIL"
+ smoke_config_validate "$image_tag" && validate_status="PASS" || validate_status="FAIL"
+ smoke_admin_api_health "$image_tag" "$admin_port" && admin_status="PASS" || admin_status="FAIL"
+
+ if extract_module_inventory "$image_tag" "$module_prefix"; then
+ inventory_status="PASS"
+ fi
+ fi
+
+ if [[ "$boot_status" == "PASS" && "$modules_status" == "PASS" && "$validate_status" == "PASS" && "$admin_status" == "PASS" && "$inventory_status" == "PASS" ]]; then
+ cell_status="PASS"
+ fi
+
+ echo "${scenario},${platform},${image_tag},${checked_plugins},${boot_status},${modules_status},${validate_status},${admin_status},${inventory_status},${cell_status}" >> "$summary_csv"
+ echo "[compat] RESULT scenario=${scenario} platform=${platform} status=${cell_status}"
+
+ if [[ "$KEEP_IMAGES" != "1" ]]; then
+ docker image rm "$image_tag" >/dev/null 2>&1 || true
+ fi
+}
+
+write_docs_report() {
+ local summary_csv="$OUTPUT_DIR/matrix-summary.csv"
+ local generated_at
+ generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+
+ {
+ echo "# PR-1 Caddy Compatibility Matrix Report"
+ echo
+ echo "- Generated at: ${generated_at}"
+ echo "- Candidate Caddy version: ${CANDIDATE_VERSION}"
+ echo "- Plugin set: ${PLUGIN_SET}"
+ echo "- Smoke set: ${SMOKE_SET}"
+ echo "- Matrix dimensions: patch scenario × platform/arch × checked plugin modules"
+ echo
+ echo "## Deterministic Pass/Fail"
+ echo
+ echo "A matrix cell is PASS only when every smoke check and module inventory extraction passes."
+ echo
+ echo "Promotion gate semantics (spec-aligned):"
+ echo "- Scenario A on linux/amd64 and linux/arm64 is promotion-gating."
+ echo "- Scenario B/C are evidence-only; failures in B/C do not fail the PR-1 promotion gate."
+ echo
+ echo "## Matrix Output"
+ echo
+ echo "| Scenario | Platform | Plugins Checked | boot_caddy | plugin_modules | config_validate | admin_api_health | module_inventory | Status |"
+ echo "| --- | --- | --- | --- | --- | --- | --- | --- | --- |"
+
+ tail -n +2 "$summary_csv" | while IFS=',' read -r scenario platform _image checked_plugins boot modules validate admin inventory status; do
+ local plugins_display
+ plugins_display="${checked_plugins//;/, }"
+ echo "| ${scenario} | ${platform} | ${plugins_display} | ${boot} | ${modules} | ${validate} | ${admin} | ${inventory} | ${status} |"
+ done
+
+ echo
+ echo "## Artifacts"
+ echo
+ echo "- Matrix CSV: ${OUTPUT_DIR}/matrix-summary.csv"
+ echo "- Per-cell module inventories: ${OUTPUT_DIR}/module-inventory-*-go-version-m.txt"
+ echo "- Per-cell Caddy module listings: ${OUTPUT_DIR}/module-inventory-*-modules.txt"
+ } > "$DOCS_REPORT"
+}
+
+main() {
+ parse_args "$@"
+
+ require_cmd docker
+ require_cmd curl
+
+ prepare_dirs
+ write_reports_header
+
+ local -a scenario_list
+ local -a platform_list
+
+ IFS=',' read -r -a scenario_list <<< "$PATCH_SCENARIOS"
+ IFS=',' read -r -a platform_list <<< "$PLATFORMS"
+
+ enforce_required_gate_dimensions scenario_list platform_list
+
+ local cell_index=0
+ local scenario
+ local platform
+
+ for scenario in "${scenario_list[@]}"; do
+ for platform in "${platform_list[@]}"; do
+ run_cell "$scenario" "$platform" "$cell_index"
+ cell_index=$((cell_index + 1))
+ done
+ done
+
+ write_docs_report
+
+ local summary_csv="$OUTPUT_DIR/matrix-summary.csv"
+ validate_matrix_completeness "$summary_csv" scenario_list platform_list
+ evaluate_promotion_gate "$summary_csv"
+}
+
+main "$@"
diff --git a/tests/core/proxy-hosts.spec.ts b/tests/core/proxy-hosts.spec.ts
index d0d352e2..6c0ba73c 100644
--- a/tests/core/proxy-hosts.spec.ts
+++ b/tests/core/proxy-hosts.spec.ts
@@ -36,6 +36,34 @@ async function dismissDomainDialog(page: Page): Promise {
}
}
+async function ensureEditableProxyHost(
+ page: Page,
+ testData: {
+ createProxyHost: (data: {
+ domain: string;
+ forwardHost: string;
+ forwardPort: number;
+ name?: string;
+ }) => Promise;
+ }
+): Promise {
+ const rows = page.locator('tbody tr');
+ if (await rows.count() === 0) {
+ await testData.createProxyHost({
+ name: `Editable Host ${Date.now()}`,
+ domain: `editable-${Date.now()}.example.test`,
+ forwardHost: '127.0.0.1',
+ forwardPort: 8080,
+ });
+
+ await page.goto('/proxy-hosts');
+ await waitForLoadingComplete(page);
+
+ const skeleton = page.locator('.animate-pulse');
+ await expect(skeleton).toHaveCount(0, { timeout: 10000 });
+ }
+}
+
test.describe('Proxy Hosts - CRUD Operations', () => {
test.beforeEach(async ({ page, adminUser }) => {
await loginUser(page, adminUser);
@@ -637,27 +665,30 @@ test.describe('Proxy Hosts - CRUD Operations', () => {
});
test.describe('Update Proxy Host', () => {
- test('should open edit modal with existing values', async ({ page }) => {
+ test.describe.configure({ mode: 'serial' });
+
+ test('should open edit modal with existing values', async ({ page, testData }) => {
await test.step('Find and click Edit button', async () => {
- const editButtons = page.getByRole('button', { name: /edit/i });
- const editCount = await editButtons.count();
+ await ensureEditableProxyHost(page, testData);
- if (editCount > 0) {
- await editButtons.first().click();
- await expect(page.getByRole('dialog')).toBeVisible(); // Wait for edit modal to open
+ const firstRow = page.locator('tbody tr').first();
+ await expect(firstRow).toBeVisible();
- // Verify form opens with "Edit" title
- const formTitle = page.getByRole('heading', { name: /edit.*proxy.*host/i });
- await expect(formTitle).toBeVisible({ timeout: 5000 });
+ const editButton = firstRow
+ .getByRole('button', { name: /edit proxy host|edit/i })
+ .first();
+ await expect(editButton).toBeVisible();
+ await editButton.click();
+ await expect(page.getByRole('dialog')).toBeVisible();
- // Verifyfields are populated
- const nameInput = page.locator('#proxy-name');
- const nameValue = await nameInput.inputValue();
- expect(nameValue.length >= 0).toBeTruthy();
+ const formTitle = page.getByRole('heading', { name: /edit.*proxy.*host/i });
+ await expect(formTitle).toBeVisible({ timeout: 5000 });
- // Close form
- await page.getByRole('button', { name: /cancel/i }).click();
- }
+ const nameInput = page.locator('#proxy-name');
+ const nameValue = await nameInput.inputValue();
+ expect(nameValue.length >= 0).toBeTruthy();
+
+ await page.getByRole('button', { name: /cancel/i }).click();
});
});
@@ -715,32 +746,32 @@ test.describe('Proxy Hosts - CRUD Operations', () => {
});
});
- test('should update forward host and port', async ({ page }) => {
+ test('should update forward host and port', async ({ page, testData }) => {
await test.step('Edit forward settings', async () => {
- const editButtons = page.getByRole('button', { name: /edit/i });
- const editCount = await editButtons.count();
+ await ensureEditableProxyHost(page, testData);
- if (editCount > 0) {
- await editButtons.first().click();
- await expect(page.getByRole('dialog')).toBeVisible(); // Wait for edit modal to open
+ const firstRow = page.locator('tbody tr').first();
+ await expect(firstRow).toBeVisible();
- // Update forward host
- const forwardHostInput = page.locator('#forward-host');
- await forwardHostInput.clear();
- await forwardHostInput.fill('192.168.1.200');
+ const editButton = firstRow
+ .getByRole('button', { name: /edit proxy host|edit/i })
+ .first();
+ await expect(editButton).toBeVisible();
+ await editButton.click();
+ await expect(page.getByRole('dialog')).toBeVisible();
- // Update forward port
- const forwardPortInput = page.locator('#forward-port');
- await forwardPortInput.clear();
- await forwardPortInput.fill('9000');
+ const forwardHostInput = page.locator('#forward-host');
+ await forwardHostInput.clear();
+ await forwardHostInput.fill('192.168.1.200');
- // Verify values
- expect(await forwardHostInput.inputValue()).toBe('192.168.1.200');
- expect(await forwardPortInput.inputValue()).toBe('9000');
+ const forwardPortInput = page.locator('#forward-port');
+ await forwardPortInput.clear();
+ await forwardPortInput.fill('9000');
- // Cancel without saving
- await page.getByRole('button', { name: /cancel/i }).click();
- }
+ expect(await forwardHostInput.inputValue()).toBe('192.168.1.200');
+ expect(await forwardPortInput.inputValue()).toBe('9000');
+
+ await page.getByRole('button', { name: /cancel/i }).click();
});
});
diff --git a/tests/settings/smtp-settings.spec.ts b/tests/settings/smtp-settings.spec.ts
index 0f76417d..3f5e88cf 100644
--- a/tests/settings/smtp-settings.spec.ts
+++ b/tests/settings/smtp-settings.spec.ts
@@ -16,7 +16,6 @@ import {
waitForLoadingComplete,
waitForToast,
waitForAPIResponse,
- clickAndWaitForResponse,
} from '../utils/wait-helpers';
test.describe('SMTP Settings', () => {
@@ -299,6 +298,8 @@ test.describe('SMTP Settings', () => {
});
test.describe('CRUD Operations', () => {
+ test.describe.configure({ mode: 'serial' });
+
/**
* Test: Save SMTP configuration
* Priority: P0
@@ -342,6 +343,8 @@ test.describe('SMTP Settings', () => {
// Flaky test - success toast timing issue. SMTP update API works correctly.
const hostInput = page.locator('#smtp-host');
+ const portInput = page.locator('#smtp-port');
+ const fromInput = page.locator('#smtp-from');
const saveButton = page.getByRole('button', { name: /save/i }).last();
let originalHost: string;
@@ -353,16 +356,21 @@ test.describe('SMTP Settings', () => {
await test.step('Update host value', async () => {
await hostInput.clear();
await hostInput.fill('updated-smtp.test.local');
+ await portInput.clear();
+ await portInput.fill('587');
+ await fromInput.clear();
+ await fromInput.fill('noreply@test.local');
await expect(hostInput).toHaveValue('updated-smtp.test.local');
});
await test.step('Save updated configuration', async () => {
- const saveResponse = await clickAndWaitForResponse(
- page,
- saveButton,
- /\/api\/v1\/settings\/smtp/
- );
- expect(saveResponse.ok()).toBeTruthy();
+ const [saveResponse] = await Promise.all([
+ page.waitForResponse(
+ (response) => response.url().includes('/api/v1/settings/smtp') && response.request().method() === 'POST'
+ ),
+ saveButton.click(),
+ ]);
+ expect(saveResponse.status()).toBe(200);
const successToast = page
.locator('[data-testid="toast-success"]')
@@ -373,7 +381,7 @@ test.describe('SMTP Settings', () => {
});
await test.step('Reload and verify persistence', async () => {
- await page.reload();
+ await page.goto('/settings/smtp', { waitUntil: 'domcontentloaded' });
await waitForLoadingComplete(page);
const newHost = await hostInput.inputValue();
From 735b9fdd0e6ddc8c98d6b13a1f755fb876d799a8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 23 Feb 2026 14:15:17 +0000
Subject: [PATCH 03/13] chore(deps): update non-major-updates
---
.github/workflows/renovate.yml | 2 +-
.github/workflows/security-pr.yml | 2 +-
frontend/package-lock.json | 206 ++++++++++++++++++++----------
frontend/package.json | 6 +-
4 files changed, 140 insertions(+), 76 deletions(-)
diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml
index 36958d43..dd73e2cd 100644
--- a/.github/workflows/renovate.yml
+++ b/.github/workflows/renovate.yml
@@ -25,7 +25,7 @@ jobs:
fetch-depth: 1
- name: Run Renovate
- uses: renovatebot/github-action@d65ef9e20512193cc070238b49c3873a361cd50c # v46.1.1
+ uses: renovatebot/github-action@8d75b92f43899d483728e9a8a7fd44238020f6e6 # v46.1.2
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml
index 94406466..b900cb70 100644
--- a/.github/workflows/security-pr.yml
+++ b/.github/workflows/security-pr.yml
@@ -280,7 +280,7 @@ jobs:
- name: Upload Trivy SARIF to GitHub Security
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
# github/codeql-action v4
- uses: github/codeql-action/upload-sarif@710e2945787622b429f8982cacb154faa182de18
+ uses: github/codeql-action/upload-sarif@4ea06e96f5e27254d0ea8ff1b6bf2051ece134f0
with:
sarif_file: 'trivy-binary-results.sarif'
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event.workflow_run.head_branch) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1a9af5e2..ef355773 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -34,7 +34,7 @@
"devDependencies": {
"@eslint/js": "^9.39.3 <10.0.0",
"@playwright/test": "^1.58.2",
- "@tailwindcss/postcss": "^4.2.0",
+ "@tailwindcss/postcss": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -50,11 +50,11 @@
"autoprefixer": "^10.4.24",
"eslint": "^9.39.3 <10.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.5.0",
+ "eslint-plugin-react-refresh": "^0.5.1",
"jsdom": "28.1.0",
"knip": "^5.85.0",
"postcss": "^8.5.6",
- "tailwindcss": "^4.2.0",
+ "tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
@@ -2929,9 +2929,9 @@
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
- "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
+ "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2941,37 +2941,37 @@
"lightningcss": "1.31.1",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.2.0"
+ "tailwindcss": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz",
- "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
+ "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.0",
- "@tailwindcss/oxide-darwin-arm64": "4.2.0",
- "@tailwindcss/oxide-darwin-x64": "4.2.0",
- "@tailwindcss/oxide-freebsd-x64": "4.2.0",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.0",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.0",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.0",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.0",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.0"
+ "@tailwindcss/oxide-android-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-x64": "4.2.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz",
- "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
+ "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
"cpu": [
"arm64"
],
@@ -2986,9 +2986,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz",
- "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
+ "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
"cpu": [
"arm64"
],
@@ -3003,9 +3003,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz",
- "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
+ "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
"cpu": [
"x64"
],
@@ -3020,9 +3020,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz",
- "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
+ "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
"cpu": [
"x64"
],
@@ -3037,9 +3037,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz",
- "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
+ "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
"cpu": [
"arm"
],
@@ -3054,9 +3054,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz",
- "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
+ "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
"cpu": [
"arm64"
],
@@ -3071,9 +3071,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz",
- "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
+ "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
"cpu": [
"arm64"
],
@@ -3088,9 +3088,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz",
- "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
+ "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
"cpu": [
"x64"
],
@@ -3105,9 +3105,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz",
- "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
+ "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
"cpu": [
"x64"
],
@@ -3122,9 +3122,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz",
- "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
+ "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -3151,10 +3151,74 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz",
- "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
+ "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
"cpu": [
"arm64"
],
@@ -3169,9 +3233,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz",
- "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
+ "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
"cpu": [
"x64"
],
@@ -3186,17 +3250,17 @@
}
},
"node_modules/@tailwindcss/postcss": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.0.tgz",
- "integrity": "sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz",
+ "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
- "@tailwindcss/node": "4.2.0",
- "@tailwindcss/oxide": "4.2.0",
+ "@tailwindcss/node": "4.2.1",
+ "@tailwindcss/oxide": "4.2.1",
"postcss": "^8.5.6",
- "tailwindcss": "4.2.0"
+ "tailwindcss": "4.2.1"
}
},
"node_modules/@tanstack/query-core": {
@@ -4724,13 +4788,13 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.0.tgz",
- "integrity": "sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==",
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.1.tgz",
+ "integrity": "sha512-Y5sJsreCUdGcF4mLD70iJNa47Z6CX4MsqJoJBARDC/fBhmacSby7k73UuValr0F9M7GfWKpEqS4NMsniWkVxQw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "eslint": ">=9"
+ "eslint": "^9 || ^10"
}
},
"node_modules/eslint-scope": {
@@ -7097,9 +7161,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
- "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
+ "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"dev": true,
"license": "MIT"
},
diff --git a/frontend/package.json b/frontend/package.json
index 047b39b7..92f925eb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -53,7 +53,7 @@
"devDependencies": {
"@eslint/js": "^9.39.3 <10.0.0",
"@playwright/test": "^1.58.2",
- "@tailwindcss/postcss": "^4.2.0",
+ "@tailwindcss/postcss": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -69,11 +69,11 @@
"autoprefixer": "^10.4.24",
"eslint": "^9.39.3 <10.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.5.0",
+ "eslint-plugin-react-refresh": "^0.5.1",
"jsdom": "28.1.0",
"knip": "^5.85.0",
"postcss": "^8.5.6",
- "tailwindcss": "^4.2.0",
+ "tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
From 1f2b4c7d5e302ad9691dfbb190cefa25f6ff02ff Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 13:44:00 +0000
Subject: [PATCH 04/13] chore: Add Caddy compatibility gate workflow and
related scripts; update documentation and test cases
---
.../{caddy-pr1-compat.yml => caddy-compat.yml} | 10 +++++-----
.vscode/tasks.json | 2 +-
.../manual_test_pr1_caddy_compatibility_closure.md | 8 ++++----
...ibility-matrix.md => caddy-compatibility-matrix.md} | 0
scripts/caddy-compat-matrix.sh | 4 ++--
5 files changed, 12 insertions(+), 12 deletions(-)
rename .github/workflows/{caddy-pr1-compat.yml => caddy-compat.yml} (85%)
rename docs/reports/{caddy-pr1-compatibility-matrix.md => caddy-compatibility-matrix.md} (100%)
diff --git a/.github/workflows/caddy-pr1-compat.yml b/.github/workflows/caddy-compat.yml
similarity index 85%
rename from .github/workflows/caddy-pr1-compat.yml
rename to .github/workflows/caddy-compat.yml
index e5547292..df6fad27 100644
--- a/.github/workflows/caddy-pr1-compat.yml
+++ b/.github/workflows/caddy-compat.yml
@@ -1,4 +1,4 @@
-name: Caddy PR-1 Compatibility Gate
+name: Caddy Compatibility Gate
on:
pull_request:
@@ -6,7 +6,7 @@ on:
- Dockerfile
- scripts/caddy-compat-matrix.sh
- docs/plans/current_spec.md
- - .github/workflows/caddy-pr1-compat.yml
+ - .github/workflows/caddy-compat.yml
workflow_dispatch:
concurrency:
@@ -44,14 +44,14 @@ jobs:
--platforms linux/amd64,linux/arm64 \
--smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health \
--output-dir test-results/caddy-compat \
- --docs-report docs/reports/caddy-pr1-compatibility-matrix.md
+ --docs-report docs/reports/caddy-compatibility-matrix.md
- name: Upload compatibility artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
- name: caddy-pr1-compatibility-artifacts
+ name: caddy-compatibility-artifacts
path: |
test-results/caddy-compat/**
- docs/reports/caddy-pr1-compatibility-matrix.md
+ docs/reports/caddy-compatibility-matrix.md
retention-days: 14
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 735cd618..8cd3f920 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -727,7 +727,7 @@
{
"label": "Security: Caddy PR-1 Compatibility Matrix",
"type": "shell",
- "command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.1 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-pr1-compatibility-matrix.md",
+ "command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.1 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-compatibility-matrix.md",
"group": "test",
"problemMatcher": []
},
diff --git a/docs/issues/manual_test_pr1_caddy_compatibility_closure.md b/docs/issues/manual_test_pr1_caddy_compatibility_closure.md
index ecb5ef02..b46d9711 100644
--- a/docs/issues/manual_test_pr1_caddy_compatibility_closure.md
+++ b/docs/issues/manual_test_pr1_caddy_compatibility_closure.md
@@ -25,7 +25,7 @@
## Test Cases
-### TC-PR1-001 — Compatibility Matrix Completes
+### TC-001 — Compatibility Matrix Completes
- Area: Compatibility matrix
- Risk: False PASS due to partial artifacts or mixed output paths
@@ -39,7 +39,7 @@
- Status: [ ] Not run [ ] Pass [ ] Fail
- Notes:
-### TC-PR1-002 — Promotion Gate Enforces Scenario A Only
+### TC-002 — Promotion Gate Enforces Scenario A Only
- Area: Release guard
- Risk: Incorrect gate logic blocks or allows promotion unexpectedly
@@ -52,7 +52,7 @@
- Status: [ ] Not run [ ] Pass [ ] Fail
- Notes:
-### TC-PR1-003 — Candidate Build Path Is Opt-In
+### TC-003 — Candidate Build Path Is Opt-In
- Area: Candidate build path
- Risk: Candidate path becomes active without explicit opt-in
@@ -66,7 +66,7 @@
- Status: [ ] Not run [ ] Pass [ ] Fail
- Notes:
-### TC-PR1-004 — Default Runtime Behavior Does Not Drift
+### TC-004 — Default Runtime Behavior Does Not Drift
- Area: Non-drift defaults
- Risk: Silent default drift after PR-1 merge
diff --git a/docs/reports/caddy-pr1-compatibility-matrix.md b/docs/reports/caddy-compatibility-matrix.md
similarity index 100%
rename from docs/reports/caddy-pr1-compatibility-matrix.md
rename to docs/reports/caddy-compatibility-matrix.md
diff --git a/scripts/caddy-compat-matrix.sh b/scripts/caddy-compat-matrix.sh
index fb2b1fe9..bdc51524 100755
--- a/scripts/caddy-compat-matrix.sh
+++ b/scripts/caddy-compat-matrix.sh
@@ -9,7 +9,7 @@ readonly DEFAULT_PLUGIN_SET="caddy-security,coraza-caddy,caddy-crowdsec-bouncer,
readonly DEFAULT_SMOKE_SET="boot_caddy,plugin_modules,config_validate,admin_api_health"
OUTPUT_DIR="test-results/caddy-compat"
-DOCS_REPORT="docs/reports/caddy-pr1-compatibility-matrix.md"
+DOCS_REPORT="docs/reports/caddy-compatibility-matrix.md"
CANDIDATE_VERSION="$DEFAULT_CANDIDATE_VERSION"
PATCH_SCENARIOS="$DEFAULT_PATCH_SCENARIOS"
PLATFORMS="$DEFAULT_PLATFORMS"
@@ -32,7 +32,7 @@ Usage: scripts/caddy-compat-matrix.sh [options]
Options:
--output-dir Output directory (default: test-results/caddy-compat)
- --docs-report Markdown report path (default: docs/reports/caddy-pr1-compatibility-matrix.md)
+ --docs-report Markdown report path (default: docs/reports/caddy-compatibility-matrix.md)
--candidate-version Candidate Caddy version (default: 2.11.1)
--patch-scenarios Patch scenarios CSV (default: A,B,C)
--platforms Platforms CSV (default: linux/amd64,linux/arm64)
From 7b640cc0afe81f34274f975870fd656e5ef26baa Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 14:12:22 +0000
Subject: [PATCH 05/13] chore: Add Prettier and Tailwind CSS plugin to
devDependencies
---
package-lock.json | 309 ++++++++++++++++++++++++++++++----------------
package.json | 2 +
2 files changed, 205 insertions(+), 106 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 0931f742..b1bf9915 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,8 @@
"@types/node": "^25.3.0",
"dotenv": "^17.3.1",
"markdownlint-cli2": "^0.21.0",
+ "prettier": "^3.8.1",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"tar": "^7.5.9"
}
},
@@ -560,9 +562,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz",
- "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -573,9 +575,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz",
- "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -586,9 +588,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz",
- "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -599,9 +601,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz",
- "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -612,9 +614,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz",
- "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -625,9 +627,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz",
- "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -638,9 +640,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz",
- "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -651,9 +653,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz",
- "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -664,9 +666,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz",
- "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -677,9 +679,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz",
- "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -690,9 +692,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz",
- "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -703,9 +705,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz",
- "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -716,9 +718,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz",
- "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -729,9 +731,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz",
- "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -742,9 +744,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz",
- "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -755,9 +757,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz",
- "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -768,9 +770,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz",
- "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -781,9 +783,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz",
- "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -794,9 +796,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz",
- "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -807,9 +809,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz",
- "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -820,9 +822,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz",
- "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -833,9 +835,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz",
- "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -846,9 +848,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz",
- "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -859,9 +861,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz",
- "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -872,9 +874,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz",
- "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1690,9 +1692,9 @@
}
},
"node_modules/katex": {
- "version": "0.16.28",
- "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz",
- "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==",
+ "version": "0.16.32",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.32.tgz",
+ "integrity": "sha512-ac0FzkRJlpw4WyH3Zu/OgU9LmPKqjHr6O2BxfSrBt8uJ1BhvH2YK3oJ4ut/K+O+6qQt2MGpdbn0MrffVEnnUDQ==",
"dev": true,
"funding": [
"https://opencollective.com/katex",
@@ -2590,6 +2592,101 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.2.tgz",
+ "integrity": "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-hermes": "*",
+ "@prettier/plugin-oxc": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-hermes": {
+ "optional": true
+ },
+ "@prettier/plugin-oxc": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
@@ -2656,9 +2753,9 @@
}
},
"node_modules/rollup": {
- "version": "4.58.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz",
- "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -2671,31 +2768,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.58.0",
- "@rollup/rollup-android-arm64": "4.58.0",
- "@rollup/rollup-darwin-arm64": "4.58.0",
- "@rollup/rollup-darwin-x64": "4.58.0",
- "@rollup/rollup-freebsd-arm64": "4.58.0",
- "@rollup/rollup-freebsd-x64": "4.58.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.58.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.58.0",
- "@rollup/rollup-linux-arm64-gnu": "4.58.0",
- "@rollup/rollup-linux-arm64-musl": "4.58.0",
- "@rollup/rollup-linux-loong64-gnu": "4.58.0",
- "@rollup/rollup-linux-loong64-musl": "4.58.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.58.0",
- "@rollup/rollup-linux-ppc64-musl": "4.58.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.58.0",
- "@rollup/rollup-linux-riscv64-musl": "4.58.0",
- "@rollup/rollup-linux-s390x-gnu": "4.58.0",
- "@rollup/rollup-linux-x64-gnu": "4.58.0",
- "@rollup/rollup-linux-x64-musl": "4.58.0",
- "@rollup/rollup-openbsd-x64": "4.58.0",
- "@rollup/rollup-openharmony-arm64": "4.58.0",
- "@rollup/rollup-win32-arm64-msvc": "4.58.0",
- "@rollup/rollup-win32-ia32-msvc": "4.58.0",
- "@rollup/rollup-win32-x64-gnu": "4.58.0",
- "@rollup/rollup-win32-x64-msvc": "4.58.0",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
diff --git a/package.json b/package.json
index a2458c30..8f302a5c 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
"@types/node": "^25.3.0",
"dotenv": "^17.3.1",
"markdownlint-cli2": "^0.21.0",
+ "prettier": "^3.8.1",
+ "prettier-plugin-tailwindcss": "^0.7.2",
"tar": "^7.5.9"
}
}
From 79c8e660f5659b31c7dfa5a9768da37bbc53a840 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 14:23:09 +0000
Subject: [PATCH 06/13] chore: Update minimum coverage requirements to 87% for
backend and frontend tests
---
.github/skills/test-backend-coverage-scripts/run.sh | 2 +-
.github/skills/test-frontend-coverage-scripts/run.sh | 2 +-
codecov.yml | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/skills/test-backend-coverage-scripts/run.sh b/.github/skills/test-backend-coverage-scripts/run.sh
index 01b62efd..c707d78a 100755
--- a/.github/skills/test-backend-coverage-scripts/run.sh
+++ b/.github/skills/test-backend-coverage-scripts/run.sh
@@ -32,7 +32,7 @@ cd "${PROJECT_ROOT}"
validate_project_structure "backend" "scripts/go-test-coverage.sh" || error_exit "Invalid project structure"
# Set default environment variables
-set_default_env "CHARON_MIN_COVERAGE" "85"
+set_default_env "CHARON_MIN_COVERAGE" "87"
set_default_env "PERF_MAX_MS_GETSTATUS_P95" "25ms"
set_default_env "PERF_MAX_MS_GETSTATUS_P95_PARALLEL" "50ms"
set_default_env "PERF_MAX_MS_LISTDECISIONS_P95" "75ms"
diff --git a/.github/skills/test-frontend-coverage-scripts/run.sh b/.github/skills/test-frontend-coverage-scripts/run.sh
index fb81959c..90afa0e0 100755
--- a/.github/skills/test-frontend-coverage-scripts/run.sh
+++ b/.github/skills/test-frontend-coverage-scripts/run.sh
@@ -32,7 +32,7 @@ cd "${PROJECT_ROOT}"
validate_project_structure "frontend" "scripts/frontend-test-coverage.sh" || error_exit "Invalid project structure"
# Set default environment variables
-set_default_env "CHARON_MIN_COVERAGE" "85"
+set_default_env "CHARON_MIN_COVERAGE" "87"
# Execute the legacy script
log_step "EXECUTION" "Running frontend tests with coverage"
diff --git a/codecov.yml b/codecov.yml
index 9463cfb1..97e325ef 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -7,8 +7,8 @@ coverage:
status:
project:
default:
- target: 85%
- threshold: 0%
+ target: 87%
+ threshold: 1%
# Fail CI if Codecov upload/report indicates a problem
require_ci_to_pass: yes
From 63d7c5c0c4d45165788ef3b4b256f7d84e130018 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 14:40:38 +0000
Subject: [PATCH 07/13] chore: Update Caddy patch scenario and enhance
CaddyAdminAPI validation in config
---
.docker/README.md | 4 +-
.pre-commit-config.yaml | 2 +-
.version | 2 +-
Dockerfile | 4 +-
backend/internal/config/config.go | 13 +++
backend/internal/config/config_test.go | 26 +++++
...anual_test_pr2_security_posture_closure.md | 96 +++++++++++++++++++
docs/plans/current_spec.md | 85 ++++++++++++++++
docs/reports/caddy-compatibility-matrix.md | 47 +++++----
docs/reports/caddy-security-posture.md | 65 +++++++++++++
docs/reports/qa_report.md | 38 ++++----
11 files changed, 332 insertions(+), 50 deletions(-)
create mode 100644 docs/issues/manual_test_pr2_security_posture_closure.md
create mode 100644 docs/reports/caddy-security-posture.md
diff --git a/.docker/README.md b/.docker/README.md
index c92cee89..07e28903 100644
--- a/.docker/README.md
+++ b/.docker/README.md
@@ -94,7 +94,7 @@ Configure the application via `docker-compose.yml`:
| `CHARON_ENV` | `production` | Set to `development` for verbose logging (`CPM_ENV` supported for backward compatibility). |
| `CHARON_HTTP_PORT` | `8080` | Port for the Web UI (`CPM_HTTP_PORT` supported for backward compatibility). |
| `CHARON_DB_PATH` | `/app/data/charon.db` | Path to the SQLite database (`CPM_DB_PATH` supported for backward compatibility). |
-| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). |
+| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). Must resolve to an internal allowlisted host on port `2019`. |
| `CHARON_CADDY_CONFIG_ROOT` | `/config` | Path to Caddy autosave configuration directory. |
| `CHARON_CADDY_LOG_DIR` | `/var/log/caddy` | Directory for Caddy access logs. |
| `CHARON_CROWDSEC_LOG_DIR` | `/var/log/crowdsec` | Directory for CrowdSec logs. |
@@ -218,6 +218,8 @@ environment:
- CPM_CADDY_ADMIN_API=http://your-caddy-host:2019
```
+If using a non-localhost internal hostname, add it to `CHARON_SSRF_INTERNAL_HOST_ALLOWLIST`.
+
**Warning**: Charon will replace Caddy's entire configuration. Backup first!
## Performance Tuning
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 78127bdc..b48f855e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -113,7 +113,7 @@ repos:
stages: [manual] # Only runs when explicitly called
- id: frontend-type-check
name: Frontend TypeScript Check
- entry: bash -c 'cd frontend && npm run type-check'
+ entry: bash -c 'cd frontend && npx tsc --noEmit'
language: system
files: '^frontend/.*\.(ts|tsx)$'
pass_filenames: false
diff --git a/.version b/.version
index 96fb87f8..3a7f17e4 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-v0.19.0
+v0.19.1
diff --git a/Dockerfile b/Dockerfile
index 3f790457..d5088a2a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,7 +18,7 @@ ARG BUILD_DEBUG=0
ARG CADDY_VERSION=2.11.0-beta.2
ARG CADDY_CANDIDATE_VERSION=2.11.1
ARG CADDY_USE_CANDIDATE=0
-ARG CADDY_PATCH_SCENARIO=A
+ARG CADDY_PATCH_SCENARIO=B
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
@@ -252,6 +252,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# renovate: datasource=go depName=github.com/hslatman/ipstore
go get github.com/hslatman/ipstore@v0.4.0; \
if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
+ # Rollback scenario: keep explicit nebula pin if upstream compatibility regresses.
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
# uses legacy nebula APIs removed in nebula v1.10+, which causes compile
# failures in authority/provisioner. Keep this pinned to a known-compatible
@@ -259,6 +260,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# renovate: datasource=go depName=github.com/slackhq/nebula
go get github.com/slackhq/nebula@v1.9.7; \
elif [ "${CADDY_PATCH_SCENARIO}" = "B" ] || [ "${CADDY_PATCH_SCENARIO}" = "C" ]; then \
+ # Default PR-2 posture: retire explicit nebula pin and use upstream resolution.
echo "Skipping nebula pin for scenario ${CADDY_PATCH_SCENARIO}"; \
else \
echo "Unsupported CADDY_PATCH_SCENARIO=${CADDY_PATCH_SCENARIO}"; \
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index 1e2f9520..a6809456 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -7,6 +7,8 @@ import (
"path/filepath"
"strconv"
"strings"
+
+ "github.com/Wikid82/charon/backend/internal/security"
)
// Config captures runtime configuration sourced from environment variables.
@@ -106,6 +108,17 @@ func Load() (Config, error) {
Debug: getEnvAny("false", "CHARON_DEBUG", "CPM_DEBUG") == "true",
}
+ allowedInternalHosts := security.InternalServiceHostAllowlist()
+ normalizedCaddyAdminURL, err := security.ValidateInternalServiceBaseURL(
+ cfg.CaddyAdminAPI,
+ 2019,
+ allowedInternalHosts,
+ )
+ if err != nil {
+ return Config{}, fmt.Errorf("validate caddy admin api url: %w", err)
+ }
+ cfg.CaddyAdminAPI = normalizedCaddyAdminURL.String()
+
if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o700); err != nil {
return Config{}, fmt.Errorf("ensure data directory: %w", err)
}
diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go
index 4cbd3865..98597da7 100644
--- a/backend/internal/config/config_test.go
+++ b/backend/internal/config/config_test.go
@@ -258,6 +258,32 @@ func TestLoad_EmergencyConfig(t *testing.T) {
assert.Equal(t, "testpass", cfg.Emergency.BasicAuthPassword)
}
+func TestLoad_CaddyAdminAPIValidationAndNormalization(t *testing.T) {
+ tempDir := t.TempDir()
+ t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db"))
+ t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy"))
+ t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports"))
+ t.Setenv("CHARON_SSRF_INTERNAL_HOST_ALLOWLIST", "")
+ t.Setenv("CHARON_CADDY_ADMIN_API", "http://localhost:2019/config/")
+
+ cfg, err := Load()
+ require.NoError(t, err)
+ assert.Equal(t, "http://localhost:2019", cfg.CaddyAdminAPI)
+}
+
+func TestLoad_CaddyAdminAPIValidationRejectsNonAllowlistedHost(t *testing.T) {
+ tempDir := t.TempDir()
+ t.Setenv("CHARON_DB_PATH", filepath.Join(tempDir, "test.db"))
+ t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy"))
+ t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tempDir, "imports"))
+ t.Setenv("CHARON_SSRF_INTERNAL_HOST_ALLOWLIST", "")
+ t.Setenv("CHARON_CADDY_ADMIN_API", "http://example.com:2019")
+
+ _, err := Load()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "validate caddy admin api url")
+}
+
// ============================================
// splitAndTrim Tests
// ============================================
diff --git a/docs/issues/manual_test_pr2_security_posture_closure.md b/docs/issues/manual_test_pr2_security_posture_closure.md
new file mode 100644
index 00000000..0aabfc3c
--- /dev/null
+++ b/docs/issues/manual_test_pr2_security_posture_closure.md
@@ -0,0 +1,96 @@
+---
+title: "Manual Test Tracking Plan - Security Posture Closure"
+labels:
+ - testing
+ - security
+ - caddy
+priority: high
+---
+
+# Manual Test Tracking Plan - PR-2 Security Posture Closure
+
+## Scope
+PR-2 only.
+
+This plan tracks manual verification for:
+- Patch disposition decisions
+- Admin API assumptions and guardrails
+- Rollback checks
+
+Out of scope:
+- PR-1 compatibility closure tasks
+- PR-3 feature or UX expansion
+
+## Preconditions
+- [ ] Branch contains PR-2 documentation and configuration changes only.
+- [ ] Environment starts cleanly with default PR-2 settings.
+- [ ] Tester can run container start/restart and review startup logs.
+
+## Track A - Patch Disposition Validation
+
+### TC-PR2-001 Retained patches remain retained
+- [ ] Verify `expr` and `ipstore` patch decisions are documented as retained in the PR-2 security posture report.
+- [ ] Confirm no conflicting PR-2 docs state these patches are retired.
+- Expected result: retained/retained remains consistent across PR-2 closure docs.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR2-002 Nebula default retirement is clearly bounded
+- [ ] Verify PR-2 report states `nebula` retirement is by default scenario switch.
+- [ ] Verify rollback instruction is present and explicit.
+- Expected result: reviewer can identify default posture and rollback without ambiguity.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Track B - Admin API Assumption Checks
+
+### TC-PR2-003 Internal-only admin API assumption
+- [ ] Confirm PR-2 report states admin API is expected to be internal-only.
+- [ ] Confirm PR-2 QA report includes admin API validation/normalization posture.
+- Expected result: both reports communicate the same assumption.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR2-004 Invalid admin endpoint fails fast
+- [ ] Start with an intentionally invalid/non-allowlisted admin API URL.
+- [ ] Verify startup fails fast with clear configuration rejection behavior.
+- [ ] Restore valid URL and confirm startup succeeds.
+- Expected result: unsafe endpoint rejected; safe endpoint accepted.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR2-005 Port exposure assumption holds
+- [ ] Verify deployment defaults do not publish admin API port `2019`.
+- [ ] Confirm no PR-2 doc contradicts this default posture.
+- Expected result: admin API remains non-published by default.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Track C - Rollback Safety Checks
+
+### TC-PR2-006 Scenario rollback switch
+- [ ] Set `CADDY_PATCH_SCENARIO=A`.
+- [ ] Restart and verify the rollback path is accepted by the runtime.
+- [ ] Return to PR-2 default scenario and verify normal startup.
+- Expected result: rollback is deterministic and reversible.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR2-007 QA report rollback statement alignment
+- [ ] Confirm QA report and security posture report use the same rollback instruction.
+- [ ] Confirm both reports remain strictly PR-2 scoped.
+- Expected result: no conflicting rollback guidance; no PR-3 references.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Defect Log
+
+| ID | Test Case | Severity | Summary | Reproducible | Status |
+| --- | --- | --- | --- | --- | --- |
+| | | | | | |
+
+## Exit Criteria
+- [ ] All PR-2 test cases executed.
+- [ ] No unresolved critical defects.
+- [ ] Patch disposition, admin API assumptions, and rollback checks are all verified.
+- [ ] No PR-3 material introduced in this tracking plan.
diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md
index 989da5b9..06fae334 100644
--- a/docs/plans/current_spec.md
+++ b/docs/plans/current_spec.md
@@ -23,6 +23,91 @@ Status: Active and authoritative
Scope Type: Architecture/security/dependency research and implementation planning
Authority: This is the only active authoritative plan section in this file.
+## Focused Plan: GitHub Actions `setup-go` Cache Warning (`go.sum` path)
+
+Date: 2026-02-23
+Status: Planned
+Scope: Warning-only fix for GitHub Actions cache restore message:
+`Restore cache failed: Dependencies file is not found in
+/home/runner/work/Charon/Charon. Supported file pattern: go.sum`.
+
+### Introduction
+
+This focused section addresses a CI warning caused by `actions/setup-go` cache
+configuration assuming `go.sum` at repository root. Charon stores Go module
+dependencies in `backend/go.sum`.
+
+### Research Findings
+
+Verified workflow inventory (`.github/workflows/**`):
+
+- All workflows using `actions/setup-go` were identified.
+- Five workflows already set `cache-dependency-path: backend/go.sum`:
+ - `.github/workflows/codecov-upload.yml`
+ - `.github/workflows/quality-checks.yml`
+ - `.github/workflows/codeql.yml`
+ - `.github/workflows/benchmark.yml`
+ - `.github/workflows/e2e-tests-split.yml`
+- Two workflows use `actions/setup-go` without cache dependency path and are
+ the warning source:
+ - `.github/workflows/caddy-compat.yml`
+ - `.github/workflows/release-goreleaser.yml`
+- Repository check confirms only one `go.sum` exists:
+ - `backend/go.sum`
+
+### Technical Specification (Minimal Fix)
+
+Apply a warning-only cache path correction in both affected workflow steps:
+
+1. `.github/workflows/caddy-compat.yml`
+ - In `Set up Go` step, add:
+ - `cache-dependency-path: backend/go.sum`
+
+2. `.github/workflows/release-goreleaser.yml`
+ - In `Set up Go` step, add:
+ - `cache-dependency-path: backend/go.sum`
+
+No other workflow behavior, triggers, permissions, or build/test logic will be
+changed.
+
+### Implementation Plan
+
+#### Phase 1 — Workflow patch
+
+- Update only the two targeted workflow files listed above.
+
+#### Phase 2 — Validation
+
+- Run workflow YAML validation/lint checks already used by repository CI.
+- Confirm no cache restore warning appears in subsequent runs of:
+ - `Caddy Compatibility Gate`
+ - `Release (GoReleaser)`
+
+#### Phase 3 — Closeout
+
+- Mark warning remediated once both workflows execute without the missing
+ `go.sum` cache warning.
+
+### Acceptance Criteria
+
+1. Both targeted workflows include `cache-dependency-path: backend/go.sum` in
+ their `actions/setup-go` step.
+2. No unrelated workflow files are modified.
+3. No behavior changes beyond warning elimination.
+4. CI logs for affected workflows no longer show the missing dependencies-file
+ warning.
+
+### PR Slicing Strategy
+
+- Decision: Single PR.
+- Rationale: Two-line, warning-only correction in two workflow files with no
+ cross-domain behavior impact.
+- Slice:
+ - `PR-1`: Add `cache-dependency-path` to the two `setup-go` steps and verify
+ workflow run logs.
+- Rollback:
+ - Revert only these two workflow edits if unexpected cache behavior appears.
+
## Focused Remediation Plan Addendum: 3 Failing Playwright Tests
Date: 2026-02-23
diff --git a/docs/reports/caddy-compatibility-matrix.md b/docs/reports/caddy-compatibility-matrix.md
index 42fde558..15f104a4 100644
--- a/docs/reports/caddy-compatibility-matrix.md
+++ b/docs/reports/caddy-compatibility-matrix.md
@@ -1,33 +1,32 @@
-## PR-1 Caddy Compatibility Matrix
+# PR-1 Caddy Compatibility Matrix Report
-- Date: 2026-02-23
-- Candidate version: 2.11.1
-- Scope: PR-1 compatibility slice only
+- Generated at: 2026-02-23T13:52:26Z
+- Candidate Caddy version: 2.11.1
+- Plugin set: caddy-security,coraza-caddy,caddy-crowdsec-bouncer,caddy-geoip2,caddy-ratelimit
+- Smoke set: boot_caddy,plugin_modules,config_validate,admin_api_health
+- Matrix dimensions: patch scenario × platform/arch × checked plugin modules
-## Promotion Rule (PR-1)
+## Deterministic Pass/Fail
-- Promotion-gating rows: Scenario A on linux/amd64 and linux/arm64
-- Evidence-only rows: Scenario B and C
+A matrix cell is PASS only when every smoke check and module inventory extraction passes.
-## Matrix Summary
+Promotion gate semantics (spec-aligned):
+- Scenario A on linux/amd64 and linux/arm64 is promotion-gating.
+- Scenario B/C are evidence-only; failures in B/C do not fail the PR-1 promotion gate.
-| Scenario | Platform | Status | Reviewer Action |
-| --- | --- | --- | --- |
-| A | linux/amd64 | PASS | Required for promotion |
-| A | linux/arm64 | PASS | Required for promotion |
-| B | linux/amd64 | PASS | Evidence-only |
-| B | linux/arm64 | PASS | Evidence-only |
-| C | linux/amd64 | PASS | Evidence-only |
-| C | linux/arm64 | PASS | Evidence-only |
+## Matrix Output
-## Decision
-
-- Promotion gate: PASS
-- Runtime default drift: None observed in PR-1
-- Candidate path: Opt-in only
+| Scenario | Platform | Plugins Checked | boot_caddy | plugin_modules | config_validate | admin_api_health | module_inventory | Status |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| A | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
+| A | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
+| B | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
+| B | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
+| C | linux/amd64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
+| C | linux/arm64 | http.handlers.auth_portal, http.handlers.waf, http.handlers.crowdsec, http.handlers.geoip2, http.handlers.rate_limit | PASS | PASS | PASS | PASS | PASS | PASS |
## Artifacts
-- Matrix CSV: test-results/caddy-compat-closure/matrix-summary.csv
-- Module inventories: test-results/caddy-compat-closure/module-inventory-*-go-version-m.txt
-- Module listings: test-results/caddy-compat-closure/module-inventory-*-modules.txt
+- Matrix CSV: test-results/caddy-compat/matrix-summary.csv
+- Per-cell module inventories: test-results/caddy-compat/module-inventory-*-go-version-m.txt
+- Per-cell Caddy module listings: test-results/caddy-compat/module-inventory-*-modules.txt
diff --git a/docs/reports/caddy-security-posture.md b/docs/reports/caddy-security-posture.md
new file mode 100644
index 00000000..893e6d55
--- /dev/null
+++ b/docs/reports/caddy-security-posture.md
@@ -0,0 +1,65 @@
+## PR-2 Security Patch Posture and Advisory Disposition
+
+- Date: 2026-02-23
+- Scope: PR-2 only (security patch posture + xcaddy patch retirement decision)
+- Upstream target: Caddy 2.11.x line (`2.11.1` candidate in this repository)
+- Inputs:
+ - PR-1 compatibility matrix: `docs/reports/caddy-compatibility-matrix.md`
+ - Plan authority: `docs/plans/current_spec.md`
+ - Runtime and bootstrap assumptions: `.docker/docker-entrypoint.sh`, `.docker/compose/docker-compose.yml`
+
+### 1) Final patch disposition
+
+| Patch target | Decision | Rationale (evidence-backed) | Rollback path |
+| --- | --- | --- | --- |
+| `github.com/expr-lang/expr@v1.17.7` | Retain | Enforced by current builder patching and CI dependency checks. | Keep current pin. |
+| `github.com/hslatman/ipstore@v0.4.0` | Retain | No PR-2 evidence supports safe retirement. | Keep current pin. |
+| `github.com/slackhq/nebula@v1.9.7` | Retire by default | Matrix evidence supports scenario `B`/`C`; default moved to `B` with rollback preserved. | Set `CADDY_PATCH_SCENARIO=A`. |
+
+### 2) Caddy 2.11.x advisory disposition
+
+| Advisory | Component summary | Exploitability | Evidence source | Owner | Recheck cadence |
+| --- | --- | --- | --- | --- | --- |
+| `GHSA-5r3v-vc8m-m96g` (`CVE-2026-27590`) | FastCGI `split_path` confusion | Not affected | Upstream advisory + Charon runtime path review (no FastCGI transport in default generated config path) | QA_Security | weekly |
+| `GHSA-879p-475x-rqh2` (`CVE-2026-27589`) | Admin API cross-origin no-cors | Mitigated | Upstream advisory + local controls: `CHARON_CADDY_ADMIN_API` now validated against internal allowlist and expected port 2019; production compose does not publish 2019 by default | QA_Security | weekly |
+| `GHSA-x76f-jf84-rqj8` (`CVE-2026-27588`) | Host matcher case bypass | Mitigated | Upstream advisory + PR-1 Caddy 2.11.x matrix compatibility evidence and Charon route/security test reliance on upgraded line | QA_Security | release-candidate |
+| `GHSA-g7pc-pc7g-h8jh` (`CVE-2026-27587`) | Path matcher escaped-case bypass | Mitigated | Upstream advisory + PR-1 matrix evidence and maintained security enforcement suite coverage | QA_Security | release-candidate |
+| `GHSA-hffm-g8v7-wrv7` (`CVE-2026-27586`) | mTLS client-auth fail-open | Not affected | Upstream advisory + Charon default deployment model does not enable mTLS client-auth CA pool configuration by default | QA_Security | on-upstream-change |
+| `GHSA-4xrr-hq4w-6vf4` (`CVE-2026-27585`) | File matcher glob sanitization bypass | Not affected | Upstream advisory + no default Charon generated config dependency on vulnerable matcher pattern | QA_Security | on-upstream-change |
+
+### 3) Admin API exposure assumptions and hardening status
+
+- Assumption: only internal Caddy admin endpoints are valid management targets.
+- PR-2 enforcement:
+ - validate and normalize `CHARON_CADDY_ADMIN_API`/`CPM_CADDY_ADMIN_API`
+ - host allowlist + expected port `2019`
+ - fail-fast startup on invalid/non-allowlisted endpoint
+- Exposure check: production compose defaults do not publish port `2019`.
+
+### 4) Runtime safety and rollback preservation
+
+- Runtime defaults keep `expr` and `ipstore` pinned.
+- `nebula` pin retirement is controlled by scenario switch, not hard deletion.
+- Emergency rollback remains one-step: `CADDY_PATCH_SCENARIO=A`.
+
+### Validation executed for PR-2
+
+| Command / Task | Outcome |
+| --- | --- |
+| `cd /projects/Charon/backend && go test ./internal/config` | PASS |
+| VS Code task `Security: Caddy PR-1 Compatibility Matrix` | PASS (A/B/C scenarios pass on `linux/amd64` and `linux/arm64`; promotion gate PASS) |
+
+Relevant generated artifacts:
+- `docs/reports/caddy-compatibility-matrix.md`
+- `test-results/caddy-compat/matrix-summary.csv`
+- `test-results/caddy-compat/module-inventory-*-go-version-m.txt`
+- `test-results/caddy-compat/module-inventory-*-modules.txt`
+
+### Residual risks / follow-up watch
+
+1. Caddy advisories with reserved or evolving CVE enrichment may change exploitability interpretation; recheck cadence remains active.
+2. Caddy bootstrap still binds admin listener to container interface (`0.0.0.0:2019`) for compatibility, so operator misconfiguration that publishes port `2019` can expand attack surface; production compose defaults avoid publishing this port.
+
+### PR-2 closure statement
+
+PR-2 posture decisions are review-ready: patch disposition is explicit, admin API assumptions are enforced, and rollback remains deterministic. No PR-3 scope is included.
diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md
index 766482d5..799791c4 100644
--- a/docs/reports/qa_report.md
+++ b/docs/reports/qa_report.md
@@ -1,31 +1,25 @@
-## QA Report — PR-1 Caddy Compatibility Closure
+## QA Report — PR-2 Security Patch Posture Audit
- Date: 2026-02-23
-- Scope: PR-1 compatibility slice only
-- Decision: Ready to close PR-1
+- Scope: PR-2 only (security patch posture, admin API hardening, rollback viability)
+- Verdict: **READY (PASS)**
-## Reviewer Checklist
+## Gate Summary
-| Gate | Status | Reviewer Action |
+| Gate | Status | Evidence |
| --- | --- | --- |
-| Targeted Playwright blocker rerun | PASS | Confirm targeted tests are no longer failing. |
-| Compatibility matrix rerun (isolated output) | PASS | Confirm A/B/C rows exist for amd64 and arm64. |
-| Promotion guard decision | PASS | Confirm promotion depends only on Scenario A (both architectures). |
-| Non-drift runtime default | PASS | Confirm default remains non-candidate. |
-| Focused pre-commit and CodeQL findings gate | PASS | Confirm no blocking findings in this slice. |
+| Targeted E2E for PR-2 | PASS | Security settings test for Caddy Admin API URL passed (2/2). |
+| Local patch preflight artifacts | PASS | `test-results/local-patch-report.md` and `.json` regenerated. |
+| Coverage and type-check | PASS | Backend coverage 87.7% line / 87.4% statement; frontend type-check passed; frontend coverage preflight input passed (88.99% lines). |
+| Pre-commit gate | PASS | `pre-commit run --all-files` passed after resolving version and type-check hook issues. |
+| Security scans | PASS | CodeQL Go/JS CI-aligned scans passed; findings gate passed with no HIGH/CRITICAL; Trivy passed at configured severities. |
+| Runtime posture + rollback | PASS | Default scenario shifted `A -> B` for PR-2 posture; rollback remains explicit via `CADDY_PATCH_SCENARIO=A`; admin API URL now validated and normalized at config load. |
-## Evidence Snapshot
+## Resolved Items
-- Targeted rerun passed for prior blocker tests.
-- Matrix run completed with full rows and PASS outcomes in isolated output.
-- Promotion gate condition met: Scenario A passed on linux/amd64 and linux/arm64.
-- Candidate path remains opt-in; default path remains stable.
+1. `check-version-match` mismatch fixed by syncing `.version` to `v0.19.1`.
+2. `frontend-type-check` hook stabilized to `npx tsc --noEmit` for deterministic pre-commit behavior.
-## Open Risks to Monitor
+## PR-2 Closure Statement
-- Matrix artifact contamination if shared output directories are reused.
-- Candidate behavior drift if default build args are changed in future slices.
-
-## Final Verdict
-
-PR-1 closure gates are satisfied for the compatibility slice.
+All PR-2 QA/security gates required for merge are passing. No PR-3 scope is included in this report.
From 1315d7a3effb35fd333b60269e7691a3a87be77d Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 14:41:39 +0000
Subject: [PATCH 08/13] chore: Add cache dependency path for Go setup in
workflows
---
.github/workflows/caddy-compat.yml | 1 +
.github/workflows/release-goreleaser.yml | 1 +
2 files changed, 2 insertions(+)
diff --git a/.github/workflows/caddy-compat.yml b/.github/workflows/caddy-compat.yml
index df6fad27..85f74471 100644
--- a/.github/workflows/caddy-compat.yml
+++ b/.github/workflows/caddy-compat.yml
@@ -29,6 +29,7 @@ jobs:
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: '1.26.0'
+ cache-dependency-path: backend/go.sum
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
diff --git a/.github/workflows/release-goreleaser.yml b/.github/workflows/release-goreleaser.yml
index 9846b125..50120ff2 100644
--- a/.github/workflows/release-goreleaser.yml
+++ b/.github/workflows/release-goreleaser.yml
@@ -48,6 +48,7 @@ jobs:
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
+ cache-dependency-path: backend/go.sum
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
From 8fa095013864c650c46068d6bc4f3c9a13818b70 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 23 Feb 2026 14:48:33 +0000
Subject: [PATCH 09/13] chore(deps): update github/codeql-action digest to
a754a57
---
.github/workflows/security-pr.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/security-pr.yml b/.github/workflows/security-pr.yml
index b900cb70..5c3804f3 100644
--- a/.github/workflows/security-pr.yml
+++ b/.github/workflows/security-pr.yml
@@ -280,7 +280,7 @@ jobs:
- name: Upload Trivy SARIF to GitHub Security
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
# github/codeql-action v4
- uses: github/codeql-action/upload-sarif@4ea06e96f5e27254d0ea8ff1b6bf2051ece134f0
+ uses: github/codeql-action/upload-sarif@a754a57c217e908c249c8e54a503b2c84076ba6f
with:
sarif_file: 'trivy-binary-results.sarif'
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event.workflow_run.head_branch) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
From ee5350d675b338b28d6dcf1673ff1fc1c2758f9c Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Mon, 23 Feb 2026 19:33:14 +0000
Subject: [PATCH 10/13] feat: add keepalive controls to System Settings
- Introduced optional keepalive settings: `keepalive_idle` and `keepalive_count` in the Server struct.
- Implemented UI controls for keepalive settings in System Settings, including validation and persistence.
- Added localization support for new keepalive fields in multiple languages.
- Created a manual test tracking plan for verifying keepalive controls and their behavior.
- Updated existing tests to cover new functionality and ensure proper validation of keepalive inputs.
- Ensured safe defaults and fallback behavior for missing or invalid keepalive values.
---
.../internal/api/handlers/settings_handler.go | 70 +++++++++
.../api/handlers/settings_handler_test.go | 110 ++++++++++++++
backend/internal/caddy/config.go | 21 +++
.../internal/caddy/config_generate_test.go | 40 +++++
backend/internal/caddy/manager.go | 60 ++++++++
.../caddy/manager_patch_coverage_test.go | 92 ++++++++++++
backend/internal/caddy/types.go | 2 +
...ual_test_pr3_keepalive_controls_closure.md | 102 +++++++++++++
docs/plans/current_spec.md | 142 ++++++++++++++++--
docs/reports/qa_report.md | 32 ++++
frontend/src/locales/de/translation.json | 7 +
frontend/src/locales/en/translation.json | 7 +
frontend/src/locales/es/translation.json | 7 +
frontend/src/locales/fr/translation.json | 7 +
frontend/src/locales/zh/translation.json | 7 +
frontend/src/pages/SystemSettings.tsx | 60 ++++++++
.../pages/__tests__/SystemSettings.test.tsx | 100 +++++++++++-
.../system-security-settings.spec.ts | 62 +++++---
.../system-settings-feature-toggles.spec.ts | 119 ++++++---------
19 files changed, 940 insertions(+), 107 deletions(-)
create mode 100644 docs/issues/manual_test_pr3_keepalive_controls_closure.md
diff --git a/backend/internal/api/handlers/settings_handler.go b/backend/internal/api/handlers/settings_handler.go
index 7d6603fd..d2eca5a6 100644
--- a/backend/internal/api/handlers/settings_handler.go
+++ b/backend/internal/api/handlers/settings_handler.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strconv"
"strings"
"time"
@@ -37,6 +38,15 @@ type SettingsHandler struct {
DataRoot string
}
+const (
+ settingCaddyKeepaliveIdle = "caddy.keepalive_idle"
+ settingCaddyKeepaliveCount = "caddy.keepalive_count"
+ minCaddyKeepaliveIdleDuration = time.Second
+ maxCaddyKeepaliveIdleDuration = 24 * time.Hour
+ minCaddyKeepaliveCount = 1
+ maxCaddyKeepaliveCount = 100
+)
+
func NewSettingsHandler(db *gorm.DB) *SettingsHandler {
return &SettingsHandler{
DB: db,
@@ -109,6 +119,11 @@ func (h *SettingsHandler) UpdateSetting(c *gin.Context) {
}
}
+ if err := validateOptionalKeepaliveSetting(req.Key, req.Value); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
setting := models.Setting{
Key: req.Key,
Value: req.Value,
@@ -247,6 +262,10 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
}
}
+ if err := validateOptionalKeepaliveSetting(key, value); err != nil {
+ return err
+ }
+
setting := models.Setting{
Key: key,
Value: value,
@@ -284,6 +303,10 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin_whitelist"})
return
}
+ if strings.Contains(err.Error(), "invalid caddy.keepalive_idle") || strings.Contains(err.Error(), "invalid caddy.keepalive_count") {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
if respondPermissionError(c, h.SecuritySvc, "settings_save_failed", err, h.DataRoot) {
return
}
@@ -401,6 +424,53 @@ func validateAdminWhitelist(whitelist string) error {
return nil
}
+func validateOptionalKeepaliveSetting(key, value string) error {
+ switch key {
+ case settingCaddyKeepaliveIdle:
+ return validateKeepaliveIdleValue(value)
+ case settingCaddyKeepaliveCount:
+ return validateKeepaliveCountValue(value)
+ default:
+ return nil
+ }
+}
+
+func validateKeepaliveIdleValue(value string) error {
+ idle := strings.TrimSpace(value)
+ if idle == "" {
+ return nil
+ }
+
+ d, err := time.ParseDuration(idle)
+ if err != nil {
+ return fmt.Errorf("invalid caddy.keepalive_idle")
+ }
+
+ if d < minCaddyKeepaliveIdleDuration || d > maxCaddyKeepaliveIdleDuration {
+ return fmt.Errorf("invalid caddy.keepalive_idle")
+ }
+
+ return nil
+}
+
+func validateKeepaliveCountValue(value string) error {
+ raw := strings.TrimSpace(value)
+ if raw == "" {
+ return nil
+ }
+
+ count, err := strconv.Atoi(raw)
+ if err != nil {
+ return fmt.Errorf("invalid caddy.keepalive_count")
+ }
+
+ if count < minCaddyKeepaliveCount || count > maxCaddyKeepaliveCount {
+ return fmt.Errorf("invalid caddy.keepalive_count")
+ }
+
+ return nil
+}
+
func (h *SettingsHandler) syncAdminWhitelist(whitelist string) error {
return h.syncAdminWhitelistWithDB(h.DB, whitelist)
}
diff --git a/backend/internal/api/handlers/settings_handler_test.go b/backend/internal/api/handlers/settings_handler_test.go
index fdc1097d..f64f4340 100644
--- a/backend/internal/api/handlers/settings_handler_test.go
+++ b/backend/internal/api/handlers/settings_handler_test.go
@@ -413,6 +413,58 @@ func TestSettingsHandler_UpdateSetting_InvalidAdminWhitelist(t *testing.T) {
assert.Contains(t, w.Body.String(), "Invalid admin_whitelist")
}
+func TestSettingsHandler_UpdateSetting_InvalidKeepaliveIdle(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ db := setupSettingsTestDB(t)
+
+ handler := handlers.NewSettingsHandler(db)
+ router := newAdminRouter()
+ router.POST("/settings", handler.UpdateSetting)
+
+ payload := map[string]string{
+ "key": "caddy.keepalive_idle",
+ "value": "bad-duration",
+ }
+ body, _ := json.Marshal(payload)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, w.Body.String(), "invalid caddy.keepalive_idle")
+}
+
+func TestSettingsHandler_UpdateSetting_ValidKeepaliveCount(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ db := setupSettingsTestDB(t)
+
+ handler := handlers.NewSettingsHandler(db)
+ router := newAdminRouter()
+ router.POST("/settings", handler.UpdateSetting)
+
+ payload := map[string]string{
+ "key": "caddy.keepalive_count",
+ "value": "9",
+ "category": "caddy",
+ "type": "number",
+ }
+ body, _ := json.Marshal(payload)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var setting models.Setting
+ err := db.Where("key = ?", "caddy.keepalive_count").First(&setting).Error
+ assert.NoError(t, err)
+ assert.Equal(t, "9", setting.Value)
+}
+
func TestSettingsHandler_UpdateSetting_SecurityKeyInvalidatesCache(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupSettingsTestDB(t)
@@ -538,6 +590,64 @@ func TestSettingsHandler_PatchConfig_InvalidAdminWhitelist(t *testing.T) {
assert.Contains(t, w.Body.String(), "Invalid admin_whitelist")
}
+func TestSettingsHandler_PatchConfig_InvalidKeepaliveCount(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ db := setupSettingsTestDB(t)
+
+ handler := handlers.NewSettingsHandler(db)
+ router := newAdminRouter()
+ router.PATCH("/config", handler.PatchConfig)
+
+ payload := map[string]any{
+ "caddy": map[string]any{
+ "keepalive_count": 0,
+ },
+ }
+ body, _ := json.Marshal(payload)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Contains(t, w.Body.String(), "invalid caddy.keepalive_count")
+}
+
+func TestSettingsHandler_PatchConfig_ValidKeepaliveSettings(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ db := setupSettingsTestDB(t)
+
+ handler := handlers.NewSettingsHandler(db)
+ router := newAdminRouter()
+ router.PATCH("/config", handler.PatchConfig)
+
+ payload := map[string]any{
+ "caddy": map[string]any{
+ "keepalive_idle": "30s",
+ "keepalive_count": 12,
+ },
+ }
+ body, _ := json.Marshal(payload)
+
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodPatch, "/config", bytes.NewBuffer(body))
+ req.Header.Set("Content-Type", "application/json")
+ router.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var idle models.Setting
+ err := db.Where("key = ?", "caddy.keepalive_idle").First(&idle).Error
+ assert.NoError(t, err)
+ assert.Equal(t, "30s", idle.Value)
+
+ var count models.Setting
+ err = db.Where("key = ?", "caddy.keepalive_count").First(&count).Error
+ assert.NoError(t, err)
+ assert.Equal(t, "12", count.Value)
+}
+
func TestSettingsHandler_PatchConfig_ReloadFailureReturns500(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupSettingsTestDB(t)
diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go
index 60008607..63a8b893 100644
--- a/backend/internal/caddy/config.go
+++ b/backend/internal/caddy/config.go
@@ -857,6 +857,27 @@ func normalizeHeaderOps(headerOps map[string]any) {
}
}
+func applyOptionalServerKeepalive(conf *Config, keepaliveIdle string, keepaliveCount int) {
+ if conf == nil || conf.Apps.HTTP == nil || conf.Apps.HTTP.Servers == nil {
+ return
+ }
+
+ server, ok := conf.Apps.HTTP.Servers["charon_server"]
+ if !ok || server == nil {
+ return
+ }
+
+ idle := strings.TrimSpace(keepaliveIdle)
+ if idle != "" {
+ server.KeepaliveIdle = &idle
+ }
+
+ if keepaliveCount > 0 {
+ count := keepaliveCount
+ server.KeepaliveCount = &count
+ }
+}
+
// NormalizeAdvancedConfig traverses a parsed JSON advanced config (map or array)
// and normalizes any headers blocks so that header values are arrays of strings.
// It returns the modified config object which can be JSON marshaled again.
diff --git a/backend/internal/caddy/config_generate_test.go b/backend/internal/caddy/config_generate_test.go
index d913f669..c3242f65 100644
--- a/backend/internal/caddy/config_generate_test.go
+++ b/backend/internal/caddy/config_generate_test.go
@@ -103,3 +103,43 @@ func TestGenerateConfig_EmergencyRoutesBypassSecurity(t *testing.T) {
require.NotEqual(t, "crowdsec", name)
}
}
+
+func TestApplyOptionalServerKeepalive_OmitsWhenUnset(t *testing.T) {
+ cfg := &Config{
+ Apps: Apps{
+ HTTP: &HTTPApp{Servers: map[string]*Server{
+ "charon_server": {
+ Listen: []string{":80", ":443"},
+ Routes: []*Route{},
+ },
+ }},
+ },
+ }
+
+ applyOptionalServerKeepalive(cfg, "", 0)
+
+ server := cfg.Apps.HTTP.Servers["charon_server"]
+ require.Nil(t, server.KeepaliveIdle)
+ require.Nil(t, server.KeepaliveCount)
+}
+
+func TestApplyOptionalServerKeepalive_AppliesValidValues(t *testing.T) {
+ cfg := &Config{
+ Apps: Apps{
+ HTTP: &HTTPApp{Servers: map[string]*Server{
+ "charon_server": {
+ Listen: []string{":80", ":443"},
+ Routes: []*Route{},
+ },
+ }},
+ },
+ }
+
+ applyOptionalServerKeepalive(cfg, "45s", 7)
+
+ server := cfg.Apps.HTTP.Servers["charon_server"]
+ require.NotNil(t, server.KeepaliveIdle)
+ require.Equal(t, "45s", *server.KeepaliveIdle)
+ require.NotNil(t, server.KeepaliveCount)
+ require.Equal(t, 7, *server.KeepaliveCount)
+}
diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go
index 01cf5447..c2cfab9d 100644
--- a/backend/internal/caddy/manager.go
+++ b/backend/internal/caddy/manager.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"sort"
+ "strconv"
"strings"
"time"
@@ -33,6 +34,15 @@ var (
validateConfigFunc = Validate
)
+const (
+ minKeepaliveIdleDuration = time.Second
+ maxKeepaliveIdleDuration = 24 * time.Hour
+ minKeepaliveCount = 1
+ maxKeepaliveCount = 100
+ settingCaddyKeepaliveIdle = "caddy.keepalive_idle"
+ settingCaddyKeepaliveCnt = "caddy.keepalive_count"
+)
+
// DNSProviderConfig contains a DNS provider with its decrypted credentials
// for use in Caddy DNS challenge configuration generation
type DNSProviderConfig struct {
@@ -277,6 +287,18 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
// Compute effective security flags (re-read runtime overrides)
_, aclEnabled, wafEnabled, rateLimitEnabled, crowdsecEnabled := m.computeEffectiveFlags(ctx)
+ keepaliveIdle := ""
+ var keepaliveIdleSetting models.Setting
+ if err := m.db.Where("key = ?", settingCaddyKeepaliveIdle).First(&keepaliveIdleSetting).Error; err == nil {
+ keepaliveIdle = sanitizeKeepaliveIdle(keepaliveIdleSetting.Value)
+ }
+
+ keepaliveCount := 0
+ var keepaliveCountSetting models.Setting
+ if err := m.db.Where("key = ?", settingCaddyKeepaliveCnt).First(&keepaliveCountSetting).Error; err == nil {
+ keepaliveCount = sanitizeKeepaliveCount(keepaliveCountSetting.Value)
+ }
+
// Safety check: if Cerberus is enabled in DB and no admin whitelist configured,
// warn but allow initial startup to proceed. This prevents total lockout when
// the user has enabled Cerberus but hasn't configured admin_whitelist yet.
@@ -401,6 +423,8 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
return fmt.Errorf("generate config: %w", err)
}
+ applyOptionalServerKeepalive(generatedConfig, keepaliveIdle, keepaliveCount)
+
// Debug logging: WAF configuration state for troubleshooting integration issues
logger.Log().WithFields(map[string]any{
"waf_enabled": wafEnabled,
@@ -467,6 +491,42 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
return nil
}
+func sanitizeKeepaliveIdle(value string) string {
+ idle := strings.TrimSpace(value)
+ if idle == "" {
+ return ""
+ }
+
+ d, err := time.ParseDuration(idle)
+ if err != nil {
+ return ""
+ }
+
+ if d < minKeepaliveIdleDuration || d > maxKeepaliveIdleDuration {
+ return ""
+ }
+
+ return idle
+}
+
+func sanitizeKeepaliveCount(value string) int {
+ raw := strings.TrimSpace(value)
+ if raw == "" {
+ return 0
+ }
+
+ count, err := strconv.Atoi(raw)
+ if err != nil {
+ return 0
+ }
+
+ if count < minKeepaliveCount || count > maxKeepaliveCount {
+ return 0
+ }
+
+ return count
+}
+
// saveSnapshot stores the config to disk with timestamp.
func (m *Manager) saveSnapshot(conf *Config) (string, error) {
timestamp := time.Now().Unix()
diff --git a/backend/internal/caddy/manager_patch_coverage_test.go b/backend/internal/caddy/manager_patch_coverage_test.go
index d9fab970..5939b322 100644
--- a/backend/internal/caddy/manager_patch_coverage_test.go
+++ b/backend/internal/caddy/manager_patch_coverage_test.go
@@ -1,8 +1,10 @@
package caddy
import (
+ "bytes"
"context"
"encoding/base64"
+ "io"
"net/http"
"net/http/httptest"
"os"
@@ -185,3 +187,93 @@ func TestManagerApplyConfig_DNSProviders_SkipsDecryptOrJSONFailures(t *testing.T
require.Len(t, captured, 1)
require.Equal(t, uint(24), captured[0].ID)
}
+
+func TestManagerApplyConfig_MapsKeepaliveSettingsToGeneratedServer(t *testing.T) {
+ var loadBody []byte
+ caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/load" && r.Method == http.MethodPost {
+ payload, _ := io.ReadAll(r.Body)
+ loadBody = append([]byte(nil), payload...)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer caddyServer.Close()
+
+ dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
+ db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ require.NoError(t, err)
+ require.NoError(t, db.AutoMigrate(
+ &models.ProxyHost{},
+ &models.Location{},
+ &models.Setting{},
+ &models.CaddyConfig{},
+ &models.SSLCertificate{},
+ &models.SecurityConfig{},
+ &models.SecurityRuleSet{},
+ &models.SecurityDecision{},
+ &models.DNSProvider{},
+ ))
+
+ db.Create(&models.ProxyHost{DomainNames: "keepalive.example.com", ForwardHost: "127.0.0.1", ForwardPort: 8080, Enabled: true})
+ db.Create(&models.SecurityConfig{Name: "default", Enabled: true})
+ db.Create(&models.Setting{Key: settingCaddyKeepaliveIdle, Value: "45s"})
+ db.Create(&models.Setting{Key: settingCaddyKeepaliveCnt, Value: "8"})
+
+ origVal := validateConfigFunc
+ defer func() { validateConfigFunc = origVal }()
+ validateConfigFunc = func(_ *Config) error { return nil }
+
+ manager := NewManager(newTestClient(t, caddyServer.URL), db, t.TempDir(), "", false, config.SecurityConfig{CerberusEnabled: true})
+ require.NoError(t, manager.ApplyConfig(context.Background()))
+ require.NotEmpty(t, loadBody)
+
+ require.True(t, bytes.Contains(loadBody, []byte(`"keepalive_idle":"45s"`)))
+ require.True(t, bytes.Contains(loadBody, []byte(`"keepalive_count":8`)))
+}
+
+func TestManagerApplyConfig_InvalidKeepaliveSettingsFallbackToDefaults(t *testing.T) {
+ var loadBody []byte
+ caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/load" && r.Method == http.MethodPost {
+ payload, _ := io.ReadAll(r.Body)
+ loadBody = append([]byte(nil), payload...)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ defer caddyServer.Close()
+
+ dsn := "file:" + t.Name() + "_invalid?mode=memory&cache=shared"
+ db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ require.NoError(t, err)
+ require.NoError(t, db.AutoMigrate(
+ &models.ProxyHost{},
+ &models.Location{},
+ &models.Setting{},
+ &models.CaddyConfig{},
+ &models.SSLCertificate{},
+ &models.SecurityConfig{},
+ &models.SecurityRuleSet{},
+ &models.SecurityDecision{},
+ &models.DNSProvider{},
+ ))
+
+ db.Create(&models.ProxyHost{DomainNames: "invalid-keepalive.example.com", ForwardHost: "127.0.0.1", ForwardPort: 8080, Enabled: true})
+ db.Create(&models.SecurityConfig{Name: "default", Enabled: true})
+ db.Create(&models.Setting{Key: settingCaddyKeepaliveIdle, Value: "bad"})
+ db.Create(&models.Setting{Key: settingCaddyKeepaliveCnt, Value: "-1"})
+
+ origVal := validateConfigFunc
+ defer func() { validateConfigFunc = origVal }()
+ validateConfigFunc = func(_ *Config) error { return nil }
+
+ manager := NewManager(newTestClient(t, caddyServer.URL), db, t.TempDir(), "", false, config.SecurityConfig{CerberusEnabled: true})
+ require.NoError(t, manager.ApplyConfig(context.Background()))
+ require.NotEmpty(t, loadBody)
+
+ require.False(t, bytes.Contains(loadBody, []byte(`"keepalive_idle"`)))
+ require.False(t, bytes.Contains(loadBody, []byte(`"keepalive_count"`)))
+}
diff --git a/backend/internal/caddy/types.go b/backend/internal/caddy/types.go
index 5fce7ba8..474964b1 100644
--- a/backend/internal/caddy/types.go
+++ b/backend/internal/caddy/types.go
@@ -83,6 +83,8 @@ type Server struct {
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
Logs *ServerLogs `json:"logs,omitempty"`
TrustedProxies *TrustedProxies `json:"trusted_proxies,omitempty"`
+ KeepaliveIdle *string `json:"keepalive_idle,omitempty"`
+ KeepaliveCount *int `json:"keepalive_count,omitempty"`
}
// TrustedProxies defines the module for configuring trusted proxy IP ranges.
diff --git a/docs/issues/manual_test_pr3_keepalive_controls_closure.md b/docs/issues/manual_test_pr3_keepalive_controls_closure.md
new file mode 100644
index 00000000..af3ff00a
--- /dev/null
+++ b/docs/issues/manual_test_pr3_keepalive_controls_closure.md
@@ -0,0 +1,102 @@
+---
+title: "Manual Test Tracking Plan - PR-3 Keepalive Controls Closure"
+labels:
+ - testing
+ - frontend
+ - backend
+ - security
+priority: high
+---
+
+# Manual Test Tracking Plan - PR-3 Keepalive Controls Closure
+
+## Scope
+PR-3 only.
+
+This plan tracks manual verification for:
+- Keepalive control behavior in System Settings
+- Safe default/fallback behavior for missing or invalid keepalive values
+- Non-exposure constraints for deferred advanced settings
+
+Out of scope:
+- PR-1 compatibility closure tasks
+- PR-2 security posture closure tasks
+- Any new page, route, or feature expansion beyond approved PR-3 controls
+
+## Preconditions
+- [ ] Branch includes PR-3 closure changes only.
+- [ ] Environment starts cleanly.
+- [ ] Tester can access System Settings and save settings.
+- [ ] Tester can restart and re-open the app to verify persisted behavior.
+
+## Track A - Keepalive Controls
+
+### TC-PR3-001 Keepalive controls are present and editable
+- [ ] Open System Settings.
+- [ ] Verify keepalive idle and keepalive count controls are visible.
+- [ ] Enter valid values and save.
+- Expected result: values save successfully and are shown after refresh.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR3-002 Keepalive values persist across reload
+- [ ] Save valid keepalive idle and count values.
+- [ ] Refresh the page.
+- [ ] Re-open System Settings.
+- Expected result: saved values are preserved.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Track B - Safe Defaults and Fallback
+
+### TC-PR3-003 Missing keepalive input keeps safe defaults
+- [ ] Clear optional keepalive inputs (leave unset/empty where allowed).
+- [ ] Save and reload settings.
+- Expected result: app remains stable and uses safe default behavior.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR3-004 Invalid keepalive input is handled safely
+- [ ] Enter invalid keepalive values (out-of-range or malformed).
+- [ ] Attempt to save.
+- [ ] Correct the values and save again.
+- Expected result: invalid values are rejected safely; system remains stable; valid correction saves.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR3-005 Regression check after fallback path
+- [ ] Trigger one invalid save attempt.
+- [ ] Save valid values immediately after.
+- [ ] Refresh and verify current values.
+- Expected result: no stuck state; final valid values are preserved.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Track C - Non-Exposure Constraints
+
+### TC-PR3-006 Deferred advanced settings remain non-exposed
+- [ ] Review System Settings controls.
+- [ ] Confirm `trusted_proxies_unix` is not exposed.
+- [ ] Confirm certificate lifecycle internals are not exposed.
+- Expected result: only approved PR-3 keepalive controls are user-visible.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+### TC-PR3-007 Scope containment remains intact
+- [ ] Verify no new page/tab/modal was introduced for PR-3 controls.
+- [ ] Verify settings flow still uses existing System Settings experience.
+- Expected result: PR-3 remains contained to approved existing surface.
+- Status: [ ] Not run [ ] Pass [ ] Fail
+- Notes:
+
+## Defect Log
+
+| ID | Test Case | Severity | Summary | Reproducible | Status |
+| --- | --- | --- | --- | --- | --- |
+| | | | | | |
+
+## Exit Criteria
+- [ ] All PR-3 test cases executed.
+- [ ] No unresolved critical defects.
+- [ ] Keepalive controls, safe fallback/default behavior, and non-exposure constraints are verified.
+- [ ] No PR-1 or PR-2 closure tasks introduced in this PR-3 plan.
diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md
index 06fae334..a7527a07 100644
--- a/docs/plans/current_spec.md
+++ b/docs/plans/current_spec.md
@@ -649,27 +649,118 @@ Rollback notes:
- Revert patch retirement lines and keep previous pinned patch model.
-### PR-3: Optional UX/API exposure and cleanup
+### PR-3: Optional UX/API exposure and cleanup (Focused Execution Update)
-Scope:
+Decision summary:
+
+- PR-3 remains optional and value-gated.
+- Expose only controls with clear operator value on existing `SystemSettings`.
+- Keep low-value/high-risk knobs backend-default and non-exposed.
-- only approved high-value settings exposed in existing settings surface
-- backend mapping and frontend wiring using existing settings flows
-- docs and translations updates if UI text changes
+Operator-value exposure decision:
+
+| Candidate | Operator value | Decision in PR-3 |
+| --- | --- | --- |
+| `keepalive_idle`, `keepalive_count` | Helps operators tune long-lived upstream behavior (streaming, websocket-heavy, high-connection churn) without editing config by hand. | **Expose minimally** (only if PR-2 confirms stable runtime behavior). |
+| `trusted_proxies_unix` | Niche socket-chain use case, easy to misconfigure, low value for default Charon operators. | **Do not expose**; backend-default only. |
+| `renewal_window_ratio` / cert maintenance internals | Advanced certificate lifecycle tuning with low day-to-day value and higher support burden. | **Do not expose**; backend-default only. |
+
+Strict scope constraints:
+
+- No new routes, pages, tabs, or modals.
+- UI changes limited to existing `frontend/src/pages/SystemSettings.tsx` general/system section.
+- API surface remains existing settings endpoints only (`POST /settings`, `PATCH /config`).
+- Preserve backend defaults when setting is absent, empty, or invalid.
+
+Minimum viable controls (if PR-3 is activated):
+
+1. `caddy.keepalive_idle` (optional)
+ - Surface: `SystemSettings` under existing Caddy/system controls.
+ - UX: bounded select/input for duration-like value (validated server-side).
+ - Persistence: existing `updateSetting()` flow.
+2. `caddy.keepalive_count` (optional)
+ - Surface: `SystemSettings` adjacent to keepalive idle.
+ - UX: bounded numeric control (validated server-side).
+ - Persistence: existing `updateSetting()` flow.
+
+Exact files/functions/components to change:
+
+Backend (no new endpoints):
+
+1. `backend/internal/caddy/manager.go`
+ - Function: `ApplyConfig(ctx context.Context) error`
+ - Change: read optional settings keys (`caddy.keepalive_idle`, `caddy.keepalive_count`), normalize/validate parsed values, pass sanitized values into config generation.
+ - Default rule: on missing/invalid values, pass empty/zero equivalents so generated config keeps current backend-default behavior.
+2. `backend/internal/caddy/config.go`
+ - Function: `GenerateConfig(...)`
+ - Change: extend function parameters with optional keepalive values and apply them only when non-default/valid.
+ - Change location: HTTP server construction block where server-level settings (including trusted proxies) are assembled.
+3. `backend/internal/caddy/types.go`
+ - Type: `Server`
+ - Change: add optional fields required to emit keepalive keys in Caddy JSON only when provided.
+4. `backend/internal/api/handlers/settings_handler.go`
+ - Functions: `UpdateSetting(...)`, `PatchConfig(...)`
+ - Change: add narrow validation for `caddy.keepalive_idle` and `caddy.keepalive_count` to reject malformed/out-of-range values while preserving existing generic settings behavior for unrelated keys.
+
+Frontend (existing surface only):
+
+1. `frontend/src/pages/SystemSettings.tsx`
+ - Component: `SystemSettings`
+ - Change: add local state load/save wiring for optional keepalive controls using existing settings query/mutation flow.
+ - Change: render controls in existing General/System card only.
+2. `frontend/src/api/settings.ts`
+ - No contract expansion required; reuse `updateSetting(key, value, category, type)`.
+3. Localization files (labels/help text only, if controls are exposed):
+ - `frontend/src/locales/en/translation.json`
+ - `frontend/src/locales/de/translation.json`
+ - `frontend/src/locales/es/translation.json`
+ - `frontend/src/locales/fr/translation.json`
+ - `frontend/src/locales/zh/translation.json`
+
+Tests to update/add (targeted):
+
+1. `frontend/src/pages/__tests__/SystemSettings.test.tsx`
+ - Verify control rendering, default-state behavior, and save calls for optional keepalive keys.
+2. `backend/internal/caddy/config_generate_test.go`
+ - Verify keepalive keys are omitted when unset/invalid and emitted when valid.
+3. `backend/internal/api/handlers/settings_handler_test.go`
+ - Verify validation pass/fail for keepalive keys via both `UpdateSetting` and `PatchConfig` paths.
+4. Existing E2E settings coverage (no new suite)
+ - Extend existing settings-related specs only if UI controls are activated in PR-3.
Dependencies:
-- PR-2 must establish stable runtime baseline first
+- PR-2 must establish stable runtime/security baseline first.
+- PR-3 activation requires explicit operator-value confirmation from PR-2 evidence.
-Acceptance criteria:
+Acceptance criteria (PR-3 complete):
+
+1. No net-new page; all UI changes are within `SystemSettings` only.
+2. No new backend routes/endpoints; existing settings APIs are reused.
+3. Only approved controls (`caddy.keepalive_idle`, `caddy.keepalive_count`) are exposed, and exposure is allowed only if the PR-3 Value Gate checklist is fully satisfied.
+4. `trusted_proxies_unix`, `renewal_window_ratio`, and certificate-maintenance internals remain backend-default and non-exposed.
+5. Backend preserves current behavior when optional keepalive settings are absent or invalid (no generated-config drift).
+6. Unit tests pass for settings validation + config generation default/override behavior.
+7. Settings UI tests pass for load/save/default behavior on exposed controls.
+8. Deferred/non-exposed features are explicitly documented in PR notes as intentional non-goals.
+
+#### PR-3 Value Gate (required evidence and approval)
+
+Required evidence checklist (all items required):
+
+- [ ] PR-2 evidence bundle contains an explicit operator-value decision record for PR-3 controls, naming `caddy.keepalive_idle` and `caddy.keepalive_count` individually.
+- [ ] Decision record includes objective evidence for each exposed control from at least one concrete source: test/baseline artifact, compatibility/security report, or documented operator requirement.
+- [ ] PR includes before/after evidence proving scope containment: no new page, no new route, and no additional exposed Caddy keys beyond the two approved controls.
+- [ ] Validation artifacts for PR-3 are attached: backend unit tests, frontend settings tests, and generated-config assertions for default/override behavior.
+
+Approval condition (pass/fail):
-1. No net-new page; updates land in existing `SystemSettings` domain.
-2. E2E and unit tests cover newly exposed controls and defaults.
-3. Deferred features explicitly documented with rationale.
+- **Pass**: all checklist items are complete and a maintainer approval explicitly states "PR-3 Value Gate approved".
+- **Fail**: any checklist item is missing or approval text is absent; PR-3 control exposure is blocked and controls remain backend-default/non-exposed.
Rollback notes:
-- Revert UI/API additions while retaining already landed security/runtime upgrades.
+- Revert only PR-3 UI/settings mapping changes while retaining PR-1/PR-2 runtime and security upgrades.
## Config File Review and Proposed Updates
@@ -735,3 +826,32 @@ After approval of this plan:
(especially patch removals).
3. Treat PR-3 as optional and value-driven, not mandatory for the security
update itself.
+
+## PR-3 QA Closure Addendum (2026-02-23)
+
+### Scope
+
+PR-3 closure only:
+
+1. Keepalive controls (`caddy.keepalive_idle`, `caddy.keepalive_count`)
+2. Safe defaults/fallback behavior when keepalive values are missing or invalid
+3. Non-exposure constraints for deferred settings
+
+### Final QA Outcome
+
+- Verdict: **READY (PASS)**
+- Targeted PR-3 E2E rerun: **30 passed, 0 failed**
+- Local patch preflight: **PASS** with required LCOV artifact present
+- Coverage/type-check/security gates: **PASS**
+
+### Scope Guardrails Confirmed
+
+- UI scope remains constrained to existing System Settings surface.
+- No PR-3 expansion beyond approved keepalive controls.
+- Non-exposed settings remain non-exposed (`trusted_proxies_unix` and certificate lifecycle internals).
+- Safe fallback/default behavior remains intact for invalid or absent keepalive input.
+
+### Reviewer References
+
+- QA closure report: `docs/reports/qa_report.md`
+- Manual verification plan: `docs/issues/manual_test_pr3_keepalive_controls_closure.md`
diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md
index 799791c4..6b0e0eba 100644
--- a/docs/reports/qa_report.md
+++ b/docs/reports/qa_report.md
@@ -23,3 +23,35 @@
## PR-2 Closure Statement
All PR-2 QA/security gates required for merge are passing. No PR-3 scope is included in this report.
+
+---
+
+## QA Report — PR-3 Keepalive Controls Closure
+
+- Date: 2026-02-23
+- Scope: PR-3 only (keepalive controls, safe fallback/default behavior, non-exposure constraints)
+- Verdict: **READY (PASS)**
+
+## Reviewer Gate Summary (PR-3)
+
+| Gate | Status | Reviewer evidence |
+| --- | --- | --- |
+| Targeted E2E rerun | PASS | Security settings targeted rerun completed: **30 passed, 0 failed**. |
+| Local patch preflight | PASS | `frontend/coverage/lcov.info` present; `scripts/local-patch-report.sh` artifacts regenerated with `pass` status. |
+| Coverage + type-check | PASS | Frontend coverage gate passed (89% lines vs 85% minimum); type-check passed. |
+| Pre-commit + security scans | PASS | `pre-commit --all-files`, CodeQL Go/JS CI-aligned scans, findings gate, and Trivy checks passed (no HIGH/CRITICAL blockers). |
+| Final readiness | PASS | All PR-3 closure gates are green. |
+
+## Scope Guardrails Verified (PR-3)
+
+- Keepalive controls are limited to approved PR-3 scope.
+- Safe fallback behavior remains intact when keepalive values are missing or invalid.
+- Non-exposure constraints remain intact (`trusted_proxies_unix` and certificate lifecycle internals are not exposed).
+
+## Manual Verification Reference
+
+- PR-3 manual test tracking plan: `docs/issues/manual_test_pr3_keepalive_controls_closure.md`
+
+## PR-3 Closure Statement
+
+PR-3 is **ready to merge** with no open QA blockers.
diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json
index 33af5ccb..e8610749 100644
--- a/frontend/src/locales/de/translation.json
+++ b/frontend/src/locales/de/translation.json
@@ -768,6 +768,13 @@
"newTab": "Neuer Tab (Standard)",
"newWindow": "Neues Fenster",
"domainLinkBehaviorHelper": "Steuern Sie, wie Domain-Links in der Proxy-Hosts-Liste geöffnet werden.",
+ "keepaliveIdle": "Keepalive Idle (Optional)",
+ "keepaliveIdleHelper": "Optionale Caddy-Dauer (z. B. 2m, 30s). Leer lassen, um Backend-Standardwerte zu verwenden.",
+ "keepaliveIdleError": "Geben Sie eine gültige Dauer ein (z. B. 30s, 2m, 1h).",
+ "keepaliveCount": "Keepalive Count (Optional)",
+ "keepaliveCountHelper": "Optionale maximale Keepalive-Tests (1-1000). Leer lassen, um Backend-Standardwerte zu verwenden.",
+ "keepaliveCountError": "Geben Sie eine ganze Zahl zwischen 1 und 1000 ein.",
+ "keepaliveValidationFailed": "Keepalive-Einstellungen enthalten ungültige Werte.",
"languageHelper": "Wählen Sie Ihre bevorzugte Sprache. Änderungen werden sofort wirksam."
},
"applicationUrl": {
diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json
index fb769b1d..e89e2d99 100644
--- a/frontend/src/locales/en/translation.json
+++ b/frontend/src/locales/en/translation.json
@@ -876,6 +876,13 @@
"newTab": "New Tab (Default)",
"newWindow": "New Window",
"domainLinkBehaviorHelper": "Control how domain links open in the Proxy Hosts list.",
+ "keepaliveIdle": "Keepalive Idle (Optional)",
+ "keepaliveIdleHelper": "Optional Caddy duration (e.g., 2m, 30s). Leave blank to keep backend defaults.",
+ "keepaliveIdleError": "Enter a valid duration (for example: 30s, 2m, 1h).",
+ "keepaliveCount": "Keepalive Count (Optional)",
+ "keepaliveCountHelper": "Optional max keepalive probes (1-1000). Leave blank to keep backend defaults.",
+ "keepaliveCountError": "Enter a whole number between 1 and 1000.",
+ "keepaliveValidationFailed": "Keepalive settings contain invalid values.",
"languageHelper": "Select your preferred language. Changes take effect immediately."
},
"applicationUrl": {
diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json
index d30ca0f2..07593570 100644
--- a/frontend/src/locales/es/translation.json
+++ b/frontend/src/locales/es/translation.json
@@ -768,6 +768,13 @@
"newTab": "Nueva Pestaña (Por defecto)",
"newWindow": "Nueva Ventana",
"domainLinkBehaviorHelper": "Controla cómo se abren los enlaces de dominio en la lista de Hosts Proxy.",
+ "keepaliveIdle": "Keepalive Idle (Opcional)",
+ "keepaliveIdleHelper": "Duración opcional de Caddy (por ejemplo, 2m, 30s). Déjelo vacío para mantener los valores predeterminados del backend.",
+ "keepaliveIdleError": "Ingrese una duración válida (por ejemplo: 30s, 2m, 1h).",
+ "keepaliveCount": "Keepalive Count (Opcional)",
+ "keepaliveCountHelper": "Número máximo opcional de sondeos keepalive (1-1000). Déjelo vacío para mantener los valores predeterminados del backend.",
+ "keepaliveCountError": "Ingrese un número entero entre 1 y 1000.",
+ "keepaliveValidationFailed": "La configuración de keepalive contiene valores no válidos.",
"languageHelper": "Selecciona tu idioma preferido. Los cambios surten efecto inmediatamente."
}, "applicationUrl": {
"title": "URL de aplicación",
diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json
index ab379313..9853dffc 100644
--- a/frontend/src/locales/fr/translation.json
+++ b/frontend/src/locales/fr/translation.json
@@ -768,6 +768,13 @@
"newTab": "Nouvel Onglet (Par défaut)",
"newWindow": "Nouvelle Fenêtre",
"domainLinkBehaviorHelper": "Contrôle comment les liens de domaine s'ouvrent dans la liste des Hôtes Proxy.",
+ "keepaliveIdle": "Keepalive Idle (Optionnel)",
+ "keepaliveIdleHelper": "Durée Caddy optionnelle (ex. 2m, 30s). Laissez vide pour conserver les valeurs par défaut du backend.",
+ "keepaliveIdleError": "Entrez une durée valide (par exemple : 30s, 2m, 1h).",
+ "keepaliveCount": "Keepalive Count (Optionnel)",
+ "keepaliveCountHelper": "Nombre maximal optionnel de sondes keepalive (1-1000). Laissez vide pour conserver les valeurs par défaut du backend.",
+ "keepaliveCountError": "Entrez un nombre entier entre 1 et 1000.",
+ "keepaliveValidationFailed": "Les paramètres keepalive contiennent des valeurs invalides.",
"languageHelper": "Sélectionnez votre langue préférée. Les modifications prennent effet immédiatement."
}, "applicationUrl": {
"title": "URL de l'application",
diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json
index b74471c4..09e96cdd 100644
--- a/frontend/src/locales/zh/translation.json
+++ b/frontend/src/locales/zh/translation.json
@@ -768,6 +768,13 @@
"newTab": "新标签页(默认)",
"newWindow": "新窗口",
"domainLinkBehaviorHelper": "控制代理主机列表中的域名链接如何打开。",
+ "keepaliveIdle": "Keepalive Idle(可选)",
+ "keepaliveIdleHelper": "可选的 Caddy 时长(例如 2m、30s)。留空可使用后端默认值。",
+ "keepaliveIdleError": "请输入有效时长(例如:30s、2m、1h)。",
+ "keepaliveCount": "Keepalive Count(可选)",
+ "keepaliveCountHelper": "可选的 keepalive 最大探测次数(1-1000)。留空可使用后端默认值。",
+ "keepaliveCountError": "请输入 1 到 1000 之间的整数。",
+ "keepaliveValidationFailed": "keepalive 设置包含无效值。",
"languageHelper": "选择您的首选语言。更改立即生效。"
},
"applicationUrl": {
diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx
index 4cd9f1a8..3ef8a24e 100644
--- a/frontend/src/pages/SystemSettings.tsx
+++ b/frontend/src/pages/SystemSettings.tsx
@@ -41,11 +41,32 @@ export default function SystemSettings() {
const queryClient = useQueryClient()
const [caddyAdminAPI, setCaddyAdminAPI] = useState('http://localhost:2019')
const [sslProvider, setSslProvider] = useState('auto')
+ const [keepaliveIdle, setKeepaliveIdle] = useState('')
+ const [keepaliveCount, setKeepaliveCount] = useState('')
const [domainLinkBehavior, setDomainLinkBehavior] = useState('new_tab')
const [publicURL, setPublicURL] = useState('')
const [publicURLValid, setPublicURLValid] = useState(null)
const [publicURLSaving, setPublicURLSaving] = useState(false)
+ const keepaliveIdlePattern = /^(?:\d+)(?:ns|us|µs|ms|s|m|h)$/
+ const keepaliveIdleTrimmed = keepaliveIdle.trim()
+ const keepaliveCountTrimmed = keepaliveCount.trim()
+ const keepaliveIdleError =
+ keepaliveIdleTrimmed.length > 0 && !keepaliveIdlePattern.test(keepaliveIdleTrimmed)
+ ? t('systemSettings.general.keepaliveIdleError')
+ : undefined
+ const keepaliveCountError = (() => {
+ if (!keepaliveCountTrimmed) {
+ return undefined
+ }
+ const parsed = Number.parseInt(keepaliveCountTrimmed, 10)
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 1000) {
+ return t('systemSettings.general.keepaliveCountError')
+ }
+ return undefined
+ })()
+ const hasKeepaliveValidationError = Boolean(keepaliveIdleError || keepaliveCountError)
+
// Fetch Settings
const { data: settings } = useQuery({
queryKey: ['settings'],
@@ -62,6 +83,8 @@ export default function SystemSettings() {
const provider = settings['caddy.ssl_provider']
setSslProvider(validProviders.includes(provider) ? provider : 'auto')
}
+ setKeepaliveIdle(settings['caddy.keepalive_idle'] ?? '')
+ setKeepaliveCount(settings['caddy.keepalive_count'] ?? '')
if (settings['ui.domain_link_behavior']) setDomainLinkBehavior(settings['ui.domain_link_behavior'])
if (settings['app.public_url']) setPublicURL(settings['app.public_url'])
}
@@ -139,8 +162,14 @@ export default function SystemSettings() {
const saveSettingsMutation = useMutation({
mutationFn: async () => {
+ if (hasKeepaliveValidationError) {
+ throw new Error(t('systemSettings.general.keepaliveValidationFailed'))
+ }
+
await updateSetting('caddy.admin_api', caddyAdminAPI, 'caddy', 'string')
await updateSetting('caddy.ssl_provider', sslProvider, 'caddy', 'string')
+ await updateSetting('caddy.keepalive_idle', keepaliveIdleTrimmed, 'caddy', 'string')
+ await updateSetting('caddy.keepalive_count', keepaliveCountTrimmed, 'caddy', 'string')
await updateSetting('ui.domain_link_behavior', domainLinkBehavior, 'ui', 'string')
await updateSetting('app.public_url', publicURL, 'general', 'string')
},
@@ -341,6 +370,36 @@ export default function SystemSettings() {
+
+
+ setKeepaliveIdle(e.target.value)}
+ placeholder="2m"
+ error={keepaliveIdleError}
+ helperText={t('systemSettings.general.keepaliveIdleHelper')}
+ aria-invalid={keepaliveIdleError ? 'true' : 'false'}
+ />
+
+
+
+
+ setKeepaliveCount(e.target.value)}
+ placeholder="3"
+ error={keepaliveCountError}
+ helperText={t('systemSettings.general.keepaliveCountHelper')}
+ aria-invalid={keepaliveCountError ? 'true' : 'false'}
+ />
+
+
@@ -353,6 +412,7 @@ export default function SystemSettings() {