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() {