diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cb55d89..ea1950a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,9 @@ # Dependabot configuration # Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Grouping policy: collapse all minor+patch bumps into ONE PR per ecosystem so +# the review queue stays small; majors get their own grouped PR per ecosystem so +# breaking changes still get individual scrutiny. version: 2 updates: @@ -19,47 +23,17 @@ updates: - "dependencies" - "javascript" groups: - # Keep TanStack libs in lockstep — they release as a family - tanstack: + npm-minor-patch: patterns: - - "@tanstack/*" - # Tauri JS bindings - tauri: + - "*" + update-types: + - "minor" + - "patch" + npm-major: patterns: - - "@tauri-apps/*" - # Test stack - testing: - patterns: - - "vitest" - - "@vitest/*" - - "@testing-library/*" - - "jsdom" - - "@playwright/*" - # Build/lint tooling - tooling: - patterns: - - "@biomejs/*" - - "turbo" - - "vite" - - "@vitejs/*" - - "typescript" - # React core - react: - patterns: - - "react" - - "react-dom" - - "@types/react" - - "@types/react-dom" - # Semantic-release ecosystem - semantic-release: - patterns: - - "semantic-release" - - "@semantic-release/*" - # Commitlint + husky - commit-tooling: - patterns: - - "@commitlint/*" - - "husky" + - "*" + update-types: + - "major" # ─── Rust / Cargo (Tauri desktop shell) ─────────────────────────────────── - package-ecosystem: "cargo" @@ -77,16 +51,17 @@ updates: - "dependencies" - "rust" groups: - tauri-core: + cargo-minor-patch: patterns: - - "tauri" - - "tauri-*" - tokio: + - "*" + update-types: + - "minor" + - "patch" + cargo-major: patterns: - - "tokio" - - "tokio-*" - - "futures" - - "futures-*" + - "*" + update-types: + - "major" # ─── GitHub Actions workflow pins ───────────────────────────────────────── - package-ecosystem: "github-actions" @@ -103,3 +78,7 @@ updates: labels: - "dependencies" - "github-actions" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4591b1b..9b0394d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -16,8 +16,8 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v3 with: @@ -35,7 +35,7 @@ jobs: type=raw,value=latest type=sha,format=short - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 @@ -44,3 +44,34 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + publish-chart: + name: Package & push Helm chart to GHCR + runs-on: ubuntu-latest + needs: [publish] + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + + - uses: azure/setup-helm@v4 + + - name: Derive chart version + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR (Helm OCI) + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \ + --username "${{ github.actor }}" \ + --password-stdin + + - name: Package chart + run: | + helm package charts/openconcho \ + --version "${{ steps.version.outputs.VERSION }}" \ + --app-version "${{ steps.version.outputs.VERSION }}" + + - name: Push chart + run: | + helm push "openconcho-${{ steps.version.outputs.VERSION }}.tgz" \ + oci://ghcr.io/${{ github.repository_owner }}/charts diff --git a/AGENTS.md b/AGENTS.md index eb70138..a802716 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,11 @@ Frontend UI for self-hosted Honcho instances — browse memories, peers, session | `make typecheck` | tsc --noEmit | | `make test` | Vitest (unit + integration), excludes `e2e/` | | `make test-e2e` | Playwright e2e (uncached) | +| `make smoke-docker` | Local: build image + hermetic smoke test of the `/api` proxy (Docker required) | +| `make up` | Run the web container from source (dev-forward, builds) at :8080 | +| `make prod` | Run the web container from the published image (pulls `ghcr…:latest`) | +| `make down` | Stop + remove the web container (dev or prod) | +| `make clean` | `down` + remove the locally built image | | `make check` | lint + typecheck + test | | `pnpm --filter @openconcho/desktop cargo-check` | Local Rust/Tauri compile check before pushing desktop changes | | `pnpm --filter @openconcho/web generate:api` | Regen `src/api/schema.d.ts` from `openapi.json` | @@ -33,6 +38,7 @@ Frontend UI for self-hosted Honcho instances — browse memories, peers, session | `packages/web/src/test/` | Vitest unit/integration tests + setup | | `packages/web/e2e/` | Playwright e2e specs | | `packages/desktop/` | Tauri shell that bundles the built web app | +| `charts/openconcho/` | Helm 3 chart for self-hosting on Kubernetes (OCI artifact on GHCR) | | `.claude/rules/` | Coding conventions (auto-loaded; stack-agnostic, applies to all agents) | | `docs/` | Architecture and references | @@ -64,6 +70,7 @@ Before pushing any change under `packages/desktop/**` or `packages/desktop/src-t ## Key Constraints - **No hardcoded URLs** — connection config lives in `localStorage` under `openconcho:instances` (multi-instance store; legacy `openconcho:config` is auto-migrated) +- **Web CORS via a same-origin `/api` proxy** — the web build issues all Honcho calls to `/api/*` with an `X-Honcho-Upstream` header (the active instance's URL); nginx (docker) and a Vite middleware (dev) forward server-side. Transport is resolved by `dispatchFor` in `src/lib/dispatch.ts`: web → relative `/api` + header; Tauri → absolute URL + reqwest. Optional `OPENCONCHO_UPSTREAM_ALLOWLIST` guards the proxy when exposed. - **Local git hooks** — `.husky/pre-commit` runs a secret scan + Biome on staged files; `.husky/pre-push` runs `pnpm check`. Your commits and pushes trigger these. - **TanStack Router flat-route params** — always cast `params` as `as never` at `navigate()` and `` callsites - **`framer-motion` Variants typing** — import `type Variants` and annotate objects; never use `as const` on variant objects diff --git a/CHANGELOG.md b/CHANGELOG.md index 281204a..36e1884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +# [0.15.0](https://github.com/offendingcommit/openconcho/compare/v0.14.0...v0.15.0) (2026-06-03) + + +### Bug Fixes + +* **helm:** guard tmpfs blocks when empty, cap volume names at 63 chars ([d5a65d7](https://github.com/offendingcommit/openconcho/commit/d5a65d73b59378f5ce39bf76e0572da478cecbda)) +* **helm:** pdb mutual exclusion, ingress null rules guard, hpa nil utilization guard ([b4939bd](https://github.com/offendingcommit/openconcho/commit/b4939bd57f2dba5ebca9efcd42901457512e70e4)) +* **helm:** pin busybox:1.36, add -T 10 timeout, use --spider, add activeDeadlineSeconds ([8fac5d0](https://github.com/offendingcommit/openconcho/commit/8fac5d060f45b68141917efad4afe499ca2fda56)) +* **helm:** use http://json-schema.org/draft-07/schema# for Helm compatibility ([8d41455](https://github.com/offendingcommit/openconcho/commit/8d41455e39db51617d7476e5cc48577eb7fff158)) + + +### Features + +* **helm:** add _helpers.tpl with name, label, and imageTag partials ([0268275](https://github.com/offendingcommit/openconcho/commit/02682750ab766851570eae58eb0b92761b98724f)) +* **helm:** add Deployment template with read-only FS, tmpfs, probes ([514e1d4](https://github.com/offendingcommit/openconcho/commit/514e1d46c0248bfae5da1f2ceb12ca8799a81468)) +* **helm:** add NOTES.txt with access instructions and NetworkPolicy/Ingress warning ([ce211df](https://github.com/offendingcommit/openconcho/commit/ce211df48cc59dfe933eb7a1b1415591b0e9f7fa)) +* **helm:** add optional HPA, PDB, and NetworkPolicy templates ([b0b648b](https://github.com/offendingcommit/openconcho/commit/b0b648bdcf64732c0a713bd8e45077c5f1b39ba6)) +* **helm:** add optional Ingress template ([9aa106c](https://github.com/offendingcommit/openconcho/commit/9aa106cede7d5719ee2cbc48c1c677491deea568)) +* **helm:** add Service and ServiceAccount templates ([ee916ea](https://github.com/offendingcommit/openconcho/commit/ee916eabc485f37cdc56ffbdd8d9004f33f3a7b7)) +* **helm:** add test-healthz and test-spa-root helm test jobs ([ee4630e](https://github.com/offendingcommit/openconcho/commit/ee4630e79ca588ee0f9cb167ac0f58ae4b8223cc)) +* **helm:** chart scaffold — Chart.yaml, values, schema ([4112270](https://github.com/offendingcommit/openconcho/commit/411227046a3dee125a555a0d1a426afed0e74ec3)) + +# [0.14.0](https://github.com/offendingcommit/openconcho/compare/v0.13.1...v0.14.0) (2026-06-02) + + +### Bug Fixes + +* **docker:** derive nginx resolver from container DNS ([66b299a](https://github.com/offendingcommit/openconcho/commit/66b299a28e912bc2f8c2922b40292696c4f7d81a)) +* **docker:** drop dead HONCHO_UPSTREAM and same-origin default ([a2854ab](https://github.com/offendingcommit/openconcho/commit/a2854ab8ea0a9eec2a06838fb394a0264f7dd80d)) +* **web:** enforce upstream allowlist in vite dev proxy ([b4fac95](https://github.com/offendingcommit/openconcho/commit/b4fac95f37da3985dbc4fbf64d04dd509ec86c2c)) +* **web:** raise connection-test timeout for cold upstreams ([409d7d8](https://github.com/offendingcommit/openconcho/commit/409d7d8be7f5cc94421dce32a54105ea48bfd44b)) +* **web:** strip content-encoding from vite dev proxy responses ([6b602c0](https://github.com/offendingcommit/openconcho/commit/6b602c05bb81721dfc102b3f97112b2cf58d4d60)) + + +### Features + +* **docker:** header-driven /api reverse proxy in nginx ([753c978](https://github.com/offendingcommit/openconcho/commit/753c978f56dab61d0c15b25b56ecf438cdc5ae88)) +* **docker:** render SSRF allowlist map from env ([0af1ad9](https://github.com/offendingcommit/openconcho/commit/0af1ad923cd2aa61a201d65ce4f19acb13858790)) +* **docker:** split compose into dev-forward build and prod pull ([c9bd2db](https://github.com/offendingcommit/openconcho/commit/c9bd2db07d84e0eedffeadcc6f2bc15c628eb251)) +* **web:** add dispatchFor transport helper for same-origin proxy ([9945e4c](https://github.com/offendingcommit/openconcho/commit/9945e4cf148aec6fc47bb853e8661c339c52ff32)) +* **web:** dev /api proxy middleware mirroring nginx ([ab8a1ba](https://github.com/offendingcommit/openconcho/commit/ab8a1ba866728ff972544c1d912fed59ba03a4a7)) +* **web:** route checkConnection and discovery through the proxy ([9893230](https://github.com/offendingcommit/openconcho/commit/9893230cde3d11ce73350bd12fffae236ee9adff)) +* **web:** route web build through same-origin /api proxy ([0935099](https://github.com/offendingcommit/openconcho/commit/0935099bc28468a21183f5f03105645f4ac8aa8a)) + ## [0.13.1](https://github.com/offendingcommit/openconcho/compare/v0.13.0...v0.13.1) (2026-05-29) diff --git a/Dockerfile b/Dockerfile index 8c15d47..6ac2e92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,9 +37,10 @@ COPY --chown=101:101 docker/nginx.conf.template /etc/nginx/templates/default.con # --chmod=0755 so nginx's docker-entrypoint.d actually executes it. COPY --chown=101:101 --chmod=0755 docker/40-openconcho-config.sh /docker-entrypoint.d/40-openconcho-config.sh -# Defaults target the Honcho service in a typical Compose stack; override per deploy. -ENV HONCHO_UPSTREAM=http://api:8000 \ - OPENCONCHO_DEFAULT_HONCHO_URL=same-origin +# Empty default → clean first run (configure the instance in Settings). Override per +# deploy to seed the first instance; the browser routes via /api with an +# X-Honcho-Upstream header. Optional OPENCONCHO_UPSTREAM_ALLOWLIST guards the proxy. +ENV OPENCONCHO_DEFAULT_HONCHO_URL="" EXPOSE 8080 diff --git a/Makefile b/Makefile index b04432f..1f17e46 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,8 @@ .PHONY: bootstrap dev dev-web dev-desktop \ build test test-e2e lint lint-fix typecheck check \ - ci-web ci-desktop install help + ci-web ci-desktop smoke-docker \ + up prod down clean install help help: @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS=":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' @@ -47,5 +48,20 @@ ci-web: ## CI: lint + typecheck + test + build for @openconcho/web ci-desktop: ## CI: cargo-check for @openconcho/desktop pnpm ci:desktop +smoke-docker: ## Local: build the image + smoke-test the /api proxy (Docker required) + bash docker/smoke-test.sh + +up: ## Run the web container from source (dev profile, builds) at :8080 + docker compose --profile dev up -d --build + +prod: ## Run the web container from the published image (prod profile, pulls latest) + docker compose --profile prod up -d + +down: ## Stop + remove the web container (either profile) + docker compose --profile dev --profile prod down --remove-orphans + +clean: down ## down + remove the locally built image + -docker image rm openconcho-web:local + install: ## pnpm install (no playwright) pnpm install diff --git a/README.md b/README.md index f88becb..fb2dfd1 100644 --- a/README.md +++ b/README.md @@ -89,20 +89,58 @@ pnpm --filter @openconcho/desktop dev ### Docker (web app) -Run the web UI in a container — handy for adding it to a self-hosted Honcho -Compose stack. The image serves the SPA and reverse-proxies the Honcho API under -its own origin, so the browser makes same-origin requests (no CORS to configure). +The container serves the SPA and reverse-proxies the Honcho API under its own +origin: the browser calls `/api` same-origin and names the upstream in an +`X-Honcho-Upstream` header, so there's no browser CORS to configure. + +Two Compose modes (the published image is `ghcr.io/offendingcommit/openconcho-web`): ```bash -docker run --rm -p 8080:8080 \ - -e HONCHO_UPSTREAM=http://host.docker.internal:8000 \ - ghcr.io/offendingcommit/openconcho-web:latest +# Dev-forward — build from this repo and run your local changes: +OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net make up + +# Production — pull the latest published image instead of building: +OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net make prod + +make down # stop + remove (dev or prod) +make clean # down + drop the locally built image # → http://localhost:8080 ``` -To drop it into a Honcho Compose stack, use the `openconcho` service in -[`docker-compose.yml`](docker-compose.yml). Full details, env vars, and the CORS -options are in [`docs/docker.md`](docs/docker.md). +Both modes live in one [`docker-compose.yml`](docker-compose.yml) as Compose +profiles: `make up` runs the `dev` profile (`build: .`), `make prod` runs the +`prod` profile (pulls `ghcr…:latest`). `OPENCONCHO_DEFAULT_HONCHO_URL` seeds the first instance +(absolute URL); `OPENCONCHO_UPSTREAM_ALLOWLIST` is an optional SSRF guard +(comma-separated host globs) for when you expose the proxy. Full details and env +vars are in [`docs/docker.md`](docs/docker.md). + +### Kubernetes (Helm) + +The chart is published as an OCI artifact to GHCR on every tagged release. + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --create-namespace --namespace openconcho \ + --set honcho.defaultUrl=https://honcho.example.com +``` + +Enable an Ingress and TLS: + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --create-namespace --namespace openconcho \ + --set honcho.defaultUrl=https://honcho.example.com \ + --set ingress.enabled=true \ + --set ingress.className=nginx \ + --set 'ingress.hosts[0].host=openconcho.example.com' \ + --set 'ingress.hosts[0].paths[0].path=/' \ + --set 'ingress.tls[0].secretName=openconcho-tls' \ + --set 'ingress.tls[0].hosts[0]=openconcho.example.com' +``` + +Full chart documentation, configuration reference, and an ArgoCD Application example are in [`charts/openconcho/README.md`](charts/openconcho/README.md). ### Connecting to your instance diff --git a/charts/openconcho/Chart.yaml b/charts/openconcho/Chart.yaml new file mode 100644 index 0000000..12a4099 --- /dev/null +++ b/charts/openconcho/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: openconcho +description: Self-hosted UI for Honcho — browse memories, peers, sessions, conclusions, and chat with memory context. +type: application +version: 0.14.0 +appVersion: "0.14.0" +keywords: + - honcho + - memory + - ai +home: https://github.com/offendingcommit/openconcho +sources: + - https://github.com/offendingcommit/openconcho +maintainers: + - name: offendingcommit + url: https://github.com/offendingcommit diff --git a/charts/openconcho/README.md b/charts/openconcho/README.md new file mode 100644 index 0000000..82a0529 --- /dev/null +++ b/charts/openconcho/README.md @@ -0,0 +1,229 @@ +# openconcho Helm Chart + +Helm 3 chart for self-hosting the [openconcho](https://github.com/offendingcommit/openconcho) web UI on Kubernetes. + +The chart deploys a single nginx-unprivileged container (port 8080, UID 101) that serves the React SPA and reverse-proxies Honcho API calls under `/api` to avoid browser CORS issues. + +## Prerequisites + +- Kubernetes 1.25+ +- Helm 3.10+ +- A running [Honcho](https://github.com/plastic-labs/honcho) instance reachable from within the cluster (or via a configured ingress) + +## Installing + +Add the chart repository: + +```bash +helm registry login ghcr.io --username --password +``` + +Install the chart: + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --set honcho.defaultUrl=https://honcho.example.com +``` + +Or with a values file (recommended): + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + -f my-values.yaml +``` + +## Upgrading + +```bash +helm upgrade openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version \ + -f my-values.yaml +``` + +## Uninstalling + +```bash +helm uninstall openconcho +``` + +## Configuration + +All values with their defaults are documented in [`values.yaml`](values.yaml). Key options: + +| Value | Default | Description | +|---|---|---| +| `replicaCount` | `1` | Number of pod replicas | +| `image.repository` | `ghcr.io/offendingcommit/openconcho-web` | Container image | +| `image.tag` | `""` | Tag; defaults to chart `appVersion` | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `honcho.defaultUrl` | `""` | Honcho URL pre-seeded in the UI | +| `honcho.upstreamAllowlist` | `""` | SSRF guard (comma-separated host globs) | +| `service.type` | `ClusterIP` | `ClusterIP` / `NodePort` / `LoadBalancer` | +| `service.port` | `80` | Service port | +| `ingress.enabled` | `false` | Enable Ingress resource | +| `ingress.className` | `""` | IngressClass name | +| `autoscaling.enabled` | `false` | Enable HorizontalPodAutoscaler | +| `podDisruptionBudget.enabled` | `false` | Enable PodDisruptionBudget | +| `networkPolicy.enabled` | `false` | Enable NetworkPolicy (same-namespace only) | +| `resources.requests.memory` | `32Mi` | Memory request | +| `resources.limits.memory` | `128Mi` | Memory limit | + +## Examples + +### Minimal (ClusterIP, no ingress) + +```yaml +honcho: + defaultUrl: http://honcho.honcho.svc.cluster.local:8000 +``` + +### With Ingress and TLS (cert-manager) + +```yaml +honcho: + defaultUrl: https://honcho.example.com + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: openconcho-tls + hosts: + - openconcho.example.com +``` + +### With autoscaling and disruption budget + +```yaml +replicaCount: 2 + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +podDisruptionBudget: + enabled: true + minAvailable: 1 +``` + +### With NetworkPolicy + +> **Note:** When `networkPolicy.enabled=true` and `ingress.enabled=true`, you must add +> a policy that allows traffic from the ingress-controller namespace. Run +> `helm status ` for the exact `kubectl edit` command after install. + +```yaml +networkPolicy: + enabled: true + +ingress: + enabled: true + className: nginx + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix +``` + +### Private registry + +```yaml +image: + repository: registry.example.com/myorg/openconcho-web + tag: "0.14.0" + pullPolicy: Always + +imagePullSecrets: + - name: registry-credentials +``` + +## ArgoCD Application + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: openconcho + namespace: argocd +spec: + project: default + source: + repoURL: ghcr.io/offendingcommit/charts + chart: openconcho + targetRevision: 0.14.0 + helm: + valuesObject: + honcho: + defaultUrl: https://honcho.example.com + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: openconcho-tls + hosts: + - openconcho.example.com + destination: + server: https://kubernetes.default.svc + namespace: openconcho + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +``` + +> OCI chart sources require ArgoCD 2.10+ (OCI Helm support GA). + +## Helm tests + +After install, run the bundled tests to verify the deployment is healthy: + +```bash +helm test openconcho +``` + +Two test pods run and exit 0 on success: + +| Test | What it checks | +|---|---| +| `test-healthz` | `GET /healthz` body equals `ok` | +| `test-spa-root` | `GET /` returns HTTP 200 | + +Pass `--logs` to see output from failing pods: + +```bash +helm test openconcho --logs +``` + +## Security posture + +| Control | Value | +|---|---| +| Run as UID/GID | 101 (nginx-unprivileged) | +| `runAsNonRoot` | `true` | +| `readOnlyRootFilesystem` | `true` | +| Linux capabilities | all dropped | +| `seccompProfile` | `RuntimeDefault` | +| `allowPrivilegeEscalation` | `false` | +| `automountServiceAccountToken` | `false` | +| Writable paths | `/var/cache/nginx`, `/var/run`, `/tmp` (tmpfs) | diff --git a/charts/openconcho/templates/NOTES.txt b/charts/openconcho/templates/NOTES.txt new file mode 100644 index 0000000..7cc7ea7 --- /dev/null +++ b/charts/openconcho/templates/NOTES.txt @@ -0,0 +1,40 @@ +OpenConcho {{ .Chart.AppVersion }} deployed to namespace {{ .Release.Namespace }}. + +{{- if .Values.ingress.enabled }} +Access: +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if eq .Values.service.type "NodePort" }} +Access (NodePort): + export NODE_PORT=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}") + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "http://$NODE_IP:$NODE_PORT" +{{- else if eq .Values.service.type "LoadBalancer" }} +Access (LoadBalancer — IP may take a few minutes): + export LB_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} --template '{{ "{{" }}range (index .status.loadBalancer.ingress 0){{ "}}" }}{{ "{{" }}.{{ "}}" }}{{ "{{" }}end{{ "}}" }}') + echo "http://$LB_IP:{{ .Values.service.port }}" +{{- else }} +Access (port-forward): + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "openconcho.fullname" . }} 8080:{{ .Values.service.port }} + Then open http://localhost:8080 +{{- end }} + +Run Helm tests to verify the deployment: + helm test {{ .Release.Name }} + +{{- if and .Values.networkPolicy.enabled .Values.ingress.enabled }} + +WARNING: NetworkPolicy + Ingress are both enabled. +The default NetworkPolicy allows port {{ .Values.service.containerPort }} only from pods within +namespace {{ .Release.Namespace }}. Ingress controllers typically run in a separate +namespace (ingress-nginx, kube-system, etc.) and will be blocked. +To allow ingress-controller traffic, add a namespaceSelector rule: + + kubectl edit networkpolicy --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} + + # Under spec.ingress[0].from, add: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: +{{- end }} diff --git a/charts/openconcho/templates/_helpers.tpl b/charts/openconcho/templates/_helpers.tpl new file mode 100644 index 0000000..ef8fb6a --- /dev/null +++ b/charts/openconcho/templates/_helpers.tpl @@ -0,0 +1,46 @@ +{{- define "openconcho.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "openconcho.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "openconcho.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "openconcho.labels" -}} +helm.sh/chart: {{ include "openconcho.chart" . }} +{{ include "openconcho.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "openconcho.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openconcho.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "openconcho.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "openconcho.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "openconcho.imageTag" -}} +{{- .Values.image.tag | default .Chart.AppVersion }} +{{- end }} diff --git a/charts/openconcho/templates/deployment.yaml b/charts/openconcho/templates/deployment.yaml new file mode 100644 index 0000000..900d459 --- /dev/null +++ b/charts/openconcho/templates/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "openconcho.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "openconcho.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ include "openconcho.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.containerPort }} + protocol: TCP + env: + - name: OPENCONCHO_DEFAULT_HONCHO_URL + value: {{ .Values.honcho.defaultUrl | quote }} + - name: OPENCONCHO_UPSTREAM_ALLOWLIST + value: {{ .Values.honcho.upstreamAllowlist | quote }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.tmpfsMounts }} + volumeMounts: + {{- range .Values.tmpfsMounts }} + - name: {{ .mountPath | trimPrefix "/" | replace "/" "-" | trunc 63 | trimSuffix "-" }} + mountPath: {{ .mountPath }} + {{- end }} + {{- end }} + {{- if .Values.tmpfsMounts }} + volumes: + {{- range .Values.tmpfsMounts }} + - name: {{ .mountPath | trimPrefix "/" | replace "/" "-" | trunc 63 | trimSuffix "-" }} + emptyDir: + medium: Memory + {{- end }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/openconcho/templates/hpa.yaml b/charts/openconcho/templates/hpa.yaml new file mode 100644 index 0000000..43a9b83 --- /dev/null +++ b/charts/openconcho/templates/hpa.yaml @@ -0,0 +1,24 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "openconcho.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/openconcho/templates/ingress.yaml b/charts/openconcho/templates/ingress.yaml new file mode 100644 index 0000000..27404eb --- /dev/null +++ b/charts/openconcho/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "openconcho.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + {{- if .Values.ingress.hosts }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/openconcho/templates/networkpolicy.yaml b/charts/openconcho/templates/networkpolicy.yaml new file mode 100644 index 0000000..b7dfe4a --- /dev/null +++ b/charts/openconcho/templates/networkpolicy.yaml @@ -0,0 +1,20 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + ingress: + - ports: + - port: {{ .Values.service.containerPort }} + protocol: TCP + from: + - podSelector: {} +{{- end }} diff --git a/charts/openconcho/templates/pdb.yaml b/charts/openconcho/templates/pdb.yaml new file mode 100644 index 0000000..de4b44b --- /dev/null +++ b/charts/openconcho/templates/pdb.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + {{- if not (kindIs "invalid" .Values.podDisruptionBudget.maxUnavailable) }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- else }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/openconcho/templates/service.yaml b/charts/openconcho/templates/service.yaml new file mode 100644 index 0000000..a542306 --- /dev/null +++ b/charts/openconcho/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "openconcho.selectorLabels" . | nindent 4 }} diff --git a/charts/openconcho/templates/serviceaccount.yaml b/charts/openconcho/templates/serviceaccount.yaml new file mode 100644 index 0000000..433c583 --- /dev/null +++ b/charts/openconcho/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "openconcho.serviceAccountName" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/openconcho/templates/tests/test-healthz.yaml b/charts/openconcho/templates/tests/test-healthz.yaml new file mode 100644 index 0000000..5a512e4 --- /dev/null +++ b/charts/openconcho/templates/tests/test-healthz.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "openconcho.fullname" . }}-test-healthz" + labels: + {{- include "openconcho.labels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation +spec: + restartPolicy: Never + activeDeadlineSeconds: 60 + containers: + - name: healthz + image: busybox:1.36 + command: + - sh + - -c + - | + RESPONSE=$(wget -T 10 -qO- http://{{ include "openconcho.fullname" . }}:{{ .Values.service.port }}/healthz) + if [ "$RESPONSE" != "ok" ]; then + echo "FAIL: expected 'ok', got '$RESPONSE'" + exit 1 + fi + echo "PASS: /healthz returned 'ok'" diff --git a/charts/openconcho/templates/tests/test-spa-root.yaml b/charts/openconcho/templates/tests/test-spa-root.yaml new file mode 100644 index 0000000..e6acda8 --- /dev/null +++ b/charts/openconcho/templates/tests/test-spa-root.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "openconcho.fullname" . }}-test-spa-root" + labels: + {{- include "openconcho.labels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation +spec: + restartPolicy: Never + activeDeadlineSeconds: 60 + containers: + - name: spa-root + image: busybox:1.36 + command: + - sh + - -c + - | + if ! wget -T 10 -q --spider http://{{ include "openconcho.fullname" . }}:{{ .Values.service.port }}/; then + echo "FAIL: GET / did not return HTTP 200" + exit 1 + fi + echo "PASS: / returned HTTP 200" diff --git a/charts/openconcho/values.schema.json b/charts/openconcho/values.schema.json new file mode 100644 index 0000000..2b15f7b --- /dev/null +++ b/charts/openconcho/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "minimum": 1 + }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"] + } + } + }, + "service": { + "type": "object", + "required": ["type", "port", "containerPort"], + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort", "LoadBalancer"] + }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "containerPort": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "honcho": { + "type": "object", + "properties": { + "defaultUrl": { "type": "string" }, + "upstreamAllowlist": { "type": "string" } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minReplicas": { "type": "integer", "minimum": 1 }, + "maxReplicas": { "type": "integer", "minimum": 1 }, + "targetCPUUtilizationPercentage": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minAvailable": { "type": "integer", "minimum": 0 } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" } + } + } + } +} diff --git a/charts/openconcho/values.yaml b/charts/openconcho/values.yaml new file mode 100644 index 0000000..e27a03a --- /dev/null +++ b/charts/openconcho/values.yaml @@ -0,0 +1,178 @@ +# Number of pod replicas. Increase for high availability or use autoscaling instead. +replicaCount: 1 + +image: + # Container image repository. Override to use a custom registry or fork. + repository: ghcr.io/offendingcommit/openconcho-web + # Image tag. Defaults to the chart appVersion when left empty. + tag: "" + pullPolicy: IfNotPresent + +# Secrets for pulling images from private registries. +# Example: [{ name: my-registry-secret }] +imagePullSecrets: [] + +# Override the name portion used in resource names and labels. +nameOverride: "" +# Override the full resource name (normally release-name + chart-name). +fullnameOverride: "" + +serviceAccount: + # Create a dedicated ServiceAccount for the pod. + create: true + # Disable automatic ServiceAccount token mounting — the app never calls the Kubernetes API. + automount: false + # Annotations to add to the ServiceAccount (e.g. for IRSA, Workload Identity, Vault). + annotations: {} + # Use a pre-existing ServiceAccount instead of creating one. Ignored when create is true. + name: "" + +# Annotations applied to every pod (not the Deployment). Useful for Prometheus scraping, +# Vault agent injection, Datadog unified service tagging, etc. +podAnnotations: {} +# Extra labels applied to every pod. +podLabels: {} + +# Pod-level security context shared by all containers. +# UID/GID 101 matches the nginx-unprivileged base image — do not change without rebuilding. +podSecurityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + +# Container-level security context. +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + +# Directories mounted as ephemeral tmpfs (in-memory) to satisfy nginx's write requirements +# when the root filesystem is read-only. Add entries for any additional writable paths. +tmpfsMounts: + - mountPath: /var/cache/nginx + - mountPath: /var/run + - mountPath: /tmp + +service: + # Kubernetes Service type. Options: ClusterIP | NodePort | LoadBalancer + type: ClusterIP + # Port exposed by the Service (what the Ingress or other pods target). + port: 80 + # Port the container actually listens on (nginx-unprivileged default). + containerPort: 8080 + +ingress: + enabled: false + # IngressClass name. Leave empty to accept the cluster default. + # Examples: nginx | traefik | alb | kong + className: "" + # Annotations forwarded verbatim to the Ingress resource. + # Example (cert-manager + nginx-ingress): + # kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: letsencrypt-prod + annotations: {} + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix + # TLS configuration. Provide a Secret containing the certificate. + # Example: + # - secretName: openconcho-tls + # hosts: + # - openconcho.example.com + tls: [] + +honcho: + # Default Honcho instance URL pre-populated in the UI on first load. + # Users can change or add instances at runtime; this only seeds the initial value. + # Example: https://honcho.example.com + defaultUrl: "" + # Optional SSRF guard: comma-separated host globs the nginx proxy is allowed to forward to. + # Leave empty to allow any upstream. Applies only when the proxy is publicly reachable. + # Example: honcho.example.com,*.internal.example.com + upstreamAllowlist: "" + +# CPU and memory requests / limits for the web container. +# The SPA is static HTML/JS, so memory is the primary concern and CPU is negligible at rest. +resources: + requests: + cpu: 50m + memory: 32Mi + limits: + memory: 128Mi + +livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + # Scale up when average CPU utilization across pods exceeds this percentage. + targetCPUUtilizationPercentage: 80 + +podDisruptionBudget: + enabled: false + # Minimum number of pods that must remain available during voluntary disruptions + # (node drains, rolling upgrades). Set maxUnavailable instead to flip the direction. + minAvailable: 1 + # Uncomment to use maxUnavailable instead — cannot set both simultaneously. + # maxUnavailable: 1 + +# Restrict pod-to-pod traffic at the network layer. When enabled, only pods in the +# same namespace may reach the web container. +# WARNING: if ingress is also enabled, you must add a policy that allows traffic from +# the ingress-controller namespace — run `helm status ` to see the reminder. +networkPolicy: + enabled: false + +# Spread pods across failure domains (availability zones, nodes, etc.) to reduce +# the blast radius of a single-node or single-zone failure. +# Example (zone spread): +# - maxSkew: 1 +# topologyKey: topology.kubernetes.io/zone +# whenUnsatisfiable: DoNotSchedule +# labelSelector: +# matchLabels: +# app.kubernetes.io/name: openconcho +topologySpreadConstraints: [] + +# Constrain pods to nodes whose labels match these key/value pairs. +# Example: kubernetes.io/arch: amd64 +nodeSelector: {} + +# Allow pods to be scheduled on tainted nodes. +# Example: +# - key: dedicated +# operator: Equal +# value: web +# effect: NoSchedule +tolerations: [] + +# Advanced pod affinity / anti-affinity rules. +# Example (soft anti-affinity — prefer spreading across different nodes): +# podAntiAffinity: +# preferredDuringSchedulingIgnoredDuringExecution: +# - weight: 100 +# podAffinityTerm: +# labelSelector: +# matchLabels: +# app.kubernetes.io/name: openconcho +# topologyKey: kubernetes.io/hostname +affinity: {} diff --git a/docker-compose.yml b/docker-compose.yml index d1d37c2..34b0e40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,48 @@ -# Run OpenConcho's web UI with `docker compose up`. +# OpenConcho web UI — one file, two Compose profiles (dev builds, prod pulls). # -# Standalone: serves the SPA on http://localhost:8080 and reverse-proxies the -# Honcho API under the same origin (no browser CORS). By default it points at a -# Honcho running on the host at :8000 — override HONCHO_UPSTREAM for anything else: +# make up # profile dev: build from THIS repo + run → http://localhost:8080 +# make prod # profile prod: pull ghcr…:latest instead of building +# make down # stop + remove (either profile) +# make clean # down + drop the locally built image # -# HONCHO_UPSTREAM=https://honcho.example.net docker compose up +# The SPA issues all Honcho calls same-origin to /api; nginx forwards each to the +# URL named in the per-request X-Honcho-Upstream header (no browser CORS). Seed the +# first instance with OPENCONCHO_DEFAULT_HONCHO_URL: # -# To fold this into an existing Honcho Compose stack, drop the `openconcho` -# service into that project, set HONCHO_UPSTREAM to the api service -# (e.g. http://api:8000), and add `depends_on: { api: { condition: service_healthy } }`. +# OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net make up +# +# To fold into an existing Honcho Compose stack, point the seed at the api service +# (e.g. http://api:8000 — nginx resolves it on the compose network). + +# Shared config (defined once); both profiles reference it via a YAML merge. +x-openconcho: &openconcho + environment: + # Absolute URL seeding the first instance; the browser sends it as the + # X-Honcho-Upstream header and nginx forwards there (no browser CORS). + OPENCONCHO_DEFAULT_HONCHO_URL: ${OPENCONCHO_DEFAULT_HONCHO_URL:-http://host.docker.internal:8000} + # Optional SSRF guard. Unset = forward anywhere (safe for the localhost-only + # binding below). Set comma-separated host globs before exposing the proxy: + # OPENCONCHO_UPSTREAM_ALLOWLIST: honcho.example.net,*.honcho.dev + OPENCONCHO_UPSTREAM_ALLOWLIST: ${OPENCONCHO_UPSTREAM_ALLOWLIST:-} + ports: + - "127.0.0.1:8080:8080" + # Lets the default host.docker.internal upstream resolve on Linux too + # (Docker Desktop / Colima provide it automatically). + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + services: + # Dev-forward — builds from source so you run your local changes (`make up`). openconcho: + <<: *openconcho + profiles: ["dev"] + build: . + image: openconcho-web:local + + # Production — pulls the published image instead of building (`make prod`). + openconcho-prod: + <<: *openconcho + profiles: ["prod"] image: ghcr.io/offendingcommit/openconcho-web:latest - # Or build from this repo instead of pulling the published image: - # build: . - environment: - # nginx reverse-proxies /v3 and /health to this upstream (the Honcho API). - HONCHO_UPSTREAM: ${HONCHO_UPSTREAM:-http://host.docker.internal:8000} - # The SPA defaults its Honcho base URL to its own origin, so requests flow - # through the proxy above — no browser CORS, token never leaves the origin. - OPENCONCHO_DEFAULT_HONCHO_URL: same-origin - ports: - - "127.0.0.1:8080:8080" - # Lets the default host.docker.internal upstream resolve on Linux too - # (Docker Desktop / Colima provide it automatically). - extra_hosts: - - "host.docker.internal:host-gateway" - restart: unless-stopped + pull_policy: always diff --git a/docker/40-openconcho-config.sh b/docker/40-openconcho-config.sh index 864de84..88656ca 100644 --- a/docker/40-openconcho-config.sh +++ b/docker/40-openconcho-config.sh @@ -1,7 +1,8 @@ #!/bin/sh # Regenerate the SPA's runtime config from the environment at container start. # Lets one prebuilt image target any Honcho backend without a rebuild. -# OPENCONCHO_DEFAULT_HONCHO_URL — absolute URL, "same-origin", or empty. +# OPENCONCHO_DEFAULT_HONCHO_URL — absolute URL seeding the first instance, or empty. +# OPENCONCHO_UPSTREAM_ALLOWLIST — optional comma-separated host globs (SSRF guard). # Runs from /docker-entrypoint.d before nginx starts. Requires the html dir to # be writable (default); skip or bind-mount config.js when running --read-only. set -eu @@ -9,3 +10,33 @@ set -eu cat > /usr/share/nginx/html/config.js < /etc/nginx/conf.d/00-resolver.conf + +# Render the SSRF allowlist into an nginx map for $allow_upstream. +# Unset/empty OPENCONCHO_UPSTREAM_ALLOWLIST → open (default 1), fine for the +# localhost-bound default. Set it (comma-separated host globs) before exposing +# the proxy (e.g. behind a tunnel) to reject non-matching upstreams. +ALLOWLIST_CONF=/etc/nginx/conf.d/allowlist_map.conf +if [ -z "${OPENCONCHO_UPSTREAM_ALLOWLIST:-}" ]; then + printf 'map $http_x_honcho_upstream $allow_upstream { default 1; }\n' > "$ALLOWLIST_CONF" +else + { + printf 'map $http_x_honcho_upstream $allow_upstream {\n' + printf ' default 0;\n' + IFS=',' + for host in $OPENCONCHO_UPSTREAM_ALLOWLIST; do + host=$(printf '%s' "$host" | tr -d ' ') + [ -z "$host" ] && continue + esc=$(printf '%s' "$host" | sed -e 's/[.]/\\./g' -e 's#[*]#[^/]*#g') + printf ' "~^https?://%s(:[0-9]+)?(/.*)?$" 1;\n' "$esc" + done + printf '}\n' + } > "$ALLOWLIST_CONF" +fi diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template index c0dd2f6..04abd34 100644 --- a/docker/nginx.conf.template +++ b/docker/nginx.conf.template @@ -1,6 +1,6 @@ -# OpenConcho — nginx site config (envsubst template). -# The nginx image renders ${HONCHO_UPSTREAM} from the environment at start. -# Serves the React SPA and reverse-proxies the Honcho API under the same origin. +# OpenConcho — nginx site config. +# Serves the React SPA and header-driven same-origin /api proxy to Honcho. +# The browser sends X-Honcho-Upstream per request; nginx forwards server-side (no browser CORS). server { listen 8080; @@ -12,23 +12,28 @@ server { # Don't leak the nginx version. server_tokens off; - # Same-origin reverse proxy to Honcho so the browser never sees a - # cross-origin request (no CORS). The variable + Docker resolver let nginx - # start even when the upstream isn't resolvable yet, re-resolving per request. - resolver 127.0.0.11 ipv6=off valid=10s; - set $honcho_upstream "${HONCHO_UPSTREAM}"; + # The resolver (required for per-request DNS with a runtime-variable proxy_pass) + # is rendered by the entrypoint into conf.d from the container's own DNS, so it + # works on both user-defined networks (127.0.0.11) and the default bridge. - # `^~` so these win over the static-asset regex below. - location ^~ /v3/ { - proxy_pass $honcho_upstream$request_uri; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location = /health { - proxy_pass $honcho_upstream/health; - proxy_set_header Host $host; + # Header-driven same-origin proxy: the browser names the Honcho upstream per + # request via X-Honcho-Upstream, so the browser never makes a cross-origin call. + # $allow_upstream is provided by the allowlist map in conf.d (entrypoint-rendered). + location ^~ /api/ { + set $upstream $http_x_honcho_upstream; + if ($upstream = "") { + add_header X-Honcho-Proxy-Reject "no-upstream" always; + return 421; + } + if ($allow_upstream = 0) { + add_header X-Honcho-Proxy-Reject "allowlist" always; + return 403; + } + rewrite ^/api/(.*)$ /$1 break; + proxy_pass $upstream; + proxy_ssl_server_name on; + proxy_set_header Host $proxy_host; + proxy_set_header X-Honcho-Upstream ""; } # Runtime config — regenerated per container start, must never be cached. diff --git a/docker/smoke-test.sh b/docker/smoke-test.sh new file mode 100755 index 0000000..f933c63 --- /dev/null +++ b/docker/smoke-test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Hermetic container smoke test for the same-origin /api proxy. +# +# Builds the image, then stands up a stub upstream + the openconcho container on +# a shared Docker network and asserts the proxy forwards correctly. Fully +# self-contained — no external Honcho or tailnet needed. Local-only (requires a +# Docker daemon); not part of PR CI, like the desktop cargo-check preflight. +# +# Idempotent: removes its own containers/network on entry and exit. Exits non-zero +# on any failed assertion. +# +# Usage: make smoke-docker (or: bash docker/smoke-test.sh) +set -euo pipefail +cd "$(dirname "$0")/.." + +IMAGE="openconcho-web:smoke" +NET="oc-smoke-net" +UPSTREAM="oc-smoke-upstream" +APP="oc-smoke-app" +PORT="${SMOKE_PORT:-18080}" +# Echo server: returns request method/path/headers as JSON for any verb. +STUB_IMAGE="mendhak/http-https-echo:31" +FAIL=0 + +cleanup() { + docker rm -f "$APP" "$UPSTREAM" >/dev/null 2>&1 || true + docker network rm "$NET" >/dev/null 2>&1 || true +} +trap cleanup EXIT +cleanup + +wait_ready() { # url + for _ in $(seq 1 30); do + curl -fsS "$1" >/dev/null 2>&1 && return 0 + sleep 0.5 + done + echo " FAIL: container did not become ready at $1" + FAIL=1 +} + +check() { # label expected actual + if [ "$2" = "$3" ]; then echo " PASS: $1 ($3)"; else echo " FAIL: $1 — expected $2, got $3"; FAIL=1; fi +} + +echo "==> build image" +docker build -t "$IMAGE" . >/dev/null + +echo "==> create network + stub upstream" +docker network create "$NET" >/dev/null +docker run -d --name "$UPSTREAM" --network "$NET" -e HTTP_PORT=8080 "$STUB_IMAGE" >/dev/null + +echo "==> start openconcho (default-open allowlist)" +docker run -d --name "$APP" --network "$NET" -p "$PORT:8080" \ + -e "OPENCONCHO_DEFAULT_HONCHO_URL=http://$UPSTREAM:8080" "$IMAGE" >/dev/null +wait_ready "http://localhost:$PORT/healthz" + +echo "==> assertions" +check "healthz 200" 200 "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/healthz")" +check "SPA served 200" 200 "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/")" +check "config.js injected" 200 "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/config.js")" + +# Proxy forwards POST /api/v3/test -> stub, stripping the /api prefix. +body=$(curl -s "http://localhost:$PORT/api/v3/test" \ + -H "X-Honcho-Upstream: http://$UPSTREAM:8080" -H 'content-type: application/json' -X POST -d '{}') +if echo "$body" | grep -q '/v3/test'; then echo " PASS: /api forwards + strips prefix"; else echo " FAIL: forward/strip — body: $body"; FAIL=1; fi +# Routing header must NOT leak to the upstream. +if echo "$body" | grep -qi 'x-honcho-upstream'; then echo " FAIL: X-Honcho-Upstream leaked upstream"; FAIL=1; else echo " PASS: X-Honcho-Upstream cleared upstream"; fi +# Missing routing header -> 421. +check "missing header 421" 421 "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/api/v3/test" -X POST -d '{}')" + +echo "==> restart with a non-matching allowlist" +docker rm -f "$APP" >/dev/null +docker run -d --name "$APP" --network "$NET" -p "$PORT:8080" \ + -e "OPENCONCHO_UPSTREAM_ALLOWLIST=*.honcho.dev" "$IMAGE" >/dev/null +wait_ready "http://localhost:$PORT/healthz" +check "allowlist reject 403" 403 "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/api/v3/test" \ + -H "X-Honcho-Upstream: http://$UPSTREAM:8080" -X POST -d '{}')" +reject=$(curl -s -D- -o /dev/null "http://localhost:$PORT/api/v3/test" \ + -H "X-Honcho-Upstream: http://$UPSTREAM:8080" -X POST -d '{}' | grep -i 'X-Honcho-Proxy-Reject' | tr -d '\r') +if echo "$reject" | grep -qi 'allowlist'; then echo " PASS: reject sentinel header present"; else echo " FAIL: missing reject sentinel — got: $reject"; FAIL=1; fi + +if [ "$FAIL" = 0 ]; then echo "==> SMOKE TEST PASSED"; else echo "==> SMOKE TEST FAILED"; exit 1; fi diff --git a/docs/docker.md b/docs/docker.md index 321ee44..be31dd3 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -5,19 +5,55 @@ builds the static bundle, then `nginx-unprivileged` serves it on port `8080` as a non-root user) that also **reverse-proxies the Honcho API under its own origin**, so the browser never makes a cross-origin request. -## Add it to a Honcho Compose stack (recommended) +## How the proxy works -Honcho's self-hosting path is Docker Compose. Drop the `openconcho` service from -[`docker-compose.yml`](../docker-compose.yml) into the project that runs your -Honcho `api`: +The browser issues every Honcho call same-origin to `/api/*` and names the real +upstream per request in an `X-Honcho-Upstream` header (sourced from the active +instance's base URL). nginx strips `/api`, forwards to that upstream server-side, +and returns the response. Because the browser→nginx hop is same-origin, **no CORS +applies**; the nginx→Honcho hop is server-side, where CORS is irrelevant. The +frontend stays the source of truth for which instance to talk to, so the +multi-instance switcher and the Fleet view keep working. + +## Compose: dev vs prod profiles + +One [`docker-compose.yml`](../docker-compose.yml) with two profiles; the shared +config (env, ports, extra_hosts) is defined once via a YAML anchor: + +- **`dev` profile** — `build: .`, runs **your local source** (`make up`). +- **`prod` profile** — pulls the **published image** + (`ghcr.io/offendingcommit/openconcho-web:latest`, `pull_policy: always`) (`make prod`). + +```bash +make up # build from source + run → http://localhost:8080 +make prod # pull ghcr…:latest instead of building +make down # stop + remove (either profile) +make clean # down + drop the locally built image +``` + +`make up` expands to `docker compose --profile dev up -d --build` and `make prod` +to `docker compose --profile prod up -d`. A bare `docker compose up` (no profile) +starts nothing — use the make targets or pass `--profile`. Set env inline or via a +`.env` file: + +```bash +OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net make prod +``` + +The published image is multi-arch (amd64 + arm64); the first publish creates a +private GHCR package — make it public for unauthenticated pulls. + +## Add it to an existing Honcho Compose stack + +Drop the `openconcho` service into the project that runs your Honcho `api`, +pointing the seed at the api service (nginx resolves it on the compose network): ```yaml services: openconcho: image: ghcr.io/offendingcommit/openconcho-web:latest environment: - HONCHO_UPSTREAM: http://api:8000 # nginx proxies /v3 + /health here - OPENCONCHO_DEFAULT_HONCHO_URL: same-origin + OPENCONCHO_DEFAULT_HONCHO_URL: http://api:8000 ports: - "127.0.0.1:8080:8080" depends_on: @@ -26,17 +62,16 @@ services: restart: unless-stopped ``` -`OPENCONCHO_DEFAULT_HONCHO_URL: same-origin` makes the UI default its Honcho -base URL to its own origin, so API calls go through the proxy → **no browser -CORS, and the API token never leaves the origin.** The published image is -multi-arch (amd64 + arm64); the first publish creates a private GHCR package — -make it public if you want unauthenticated pulls. +`OPENCONCHO_DEFAULT_HONCHO_URL` seeds the UI's first instance with an absolute +URL. The browser sends that URL in the `X-Honcho-Upstream` header; nginx (on the +compose network) forwards to it — **no browser CORS, and the API token never +leaves the origin.** -## Standalone +## Standalone (no compose) ```bash -docker build -t openconcho-web . -docker run --rm -p 8080:8080 -e HONCHO_UPSTREAM=http://host.docker.internal:8000 openconcho-web +docker run --rm -p 8080:8080 -e OPENCONCHO_DEFAULT_HONCHO_URL=http://host.docker.internal:8000 \ + ghcr.io/offendingcommit/openconcho-web:latest # → http://localhost:8080 · GET /healthz returns "ok" ``` @@ -44,24 +79,26 @@ Runtime knobs (no rebuild needed): | Env | Default | Meaning | |-----|---------|---------| -| `HONCHO_UPSTREAM` | `http://api:8000` | Where nginx proxies `/v3` and `/health` | -| `OPENCONCHO_DEFAULT_HONCHO_URL` | `same-origin` | SPA's default base URL — `same-origin`, an absolute URL, or empty (configure in Settings) | +| `OPENCONCHO_DEFAULT_HONCHO_URL` | _(empty)_ | Absolute URL seeding the first instance; empty = configure in Settings | +| `OPENCONCHO_UPSTREAM_ALLOWLIST` | _(empty)_ | Optional SSRF guard: comma-separated host globs (e.g. `honcho.example.net,*.honcho.dev`). Empty = forward anywhere | Hardened run adds `--read-only --cap-drop ALL --security-opt no-new-privileges` -with `--tmpfs /tmp --tmpfs /var/cache/nginx`. Note: the runtime config writes -`config.js` into the web root at start, which a read-only root blocks — under -`--read-only` either bind-mount `config.js` or leave -`OPENCONCHO_DEFAULT_HONCHO_URL` empty and set the URL in Settings. +with `--tmpfs /tmp --tmpfs /var/cache/nginx`. Note: the entrypoint writes +`config.js` and the allowlist map at start, which a read-only root blocks — under +`--read-only` either bind-mount those paths or leave the env empty and configure +the URL in Settings. + +## SSRF: when to set the allowlist + +The header-driven proxy forwards to whatever upstream the client names. With the +default `127.0.0.1:8080` binding only your own machine can reach nginx, so leaving +the allowlist open is fine. **Before exposing the proxy** (e.g. behind a tunnel), +set `OPENCONCHO_UPSTREAM_ALLOWLIST` to the host globs you trust — non-matching +upstreams are rejected with `403` and an `X-Honcho-Proxy-Reject: allowlist` header. ## CORS, the short version -The desktop app routes HTTP through Rust and bypasses browser CORS; the web -build doesn't. The Compose setup above **solves CORS via the same-origin proxy** -— nothing to configure on Honcho. If instead you point the UI at a *different* -origin (absolute `OPENCONCHO_DEFAULT_HONCHO_URL` or a URL typed in Settings), -allow that origin in Honcho's FastAPI `CORSMiddleware`: - -```python -app.add_middleware(CORSMiddleware, allow_origins=["https://your-ui-origin"], - allow_methods=["*"], allow_headers=["*"]) -``` +The desktop app routes HTTP through Rust (reqwest) and bypasses browser CORS; the +web build solves it with the same-origin `/api` proxy above — **nothing to +configure on Honcho.** The proxy makes a Honcho-side `CORSMiddleware` unnecessary +regardless of which instance you point at. diff --git a/docs/superpowers/plans/2026-06-02-web-api-proxy.md b/docs/superpowers/plans/2026-06-02-web-api-proxy.md new file mode 100644 index 0000000..6ff4bbc --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-web-api-proxy.md @@ -0,0 +1,782 @@ +# Header-Driven `/api` Proxy — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Eliminate browser CORS for the web build by routing Honcho API calls through a same-origin, header-driven reverse proxy, while leaving the Tauri desktop path untouched and preserving Fleet aggregation. + +**Architecture:** A single `dispatchFor(instance)` helper decides transport at runtime — web mode returns `baseUrl="/api"` plus an `X-Honcho-Upstream` header (the real Honcho URL); Tauri mode returns the absolute URL and reqwest. nginx (docker) and a Vite middleware (dev) read the header and forward server-side, so the browser never makes a cross-origin request. Instances are still stored as absolute URLs — only dispatch changes. + +**Tech Stack:** React 19, openapi-fetch, TanStack Query, Vitest, Biome, nginx (envsubst template), Vite dev server, Tauri v2. + +**Spec:** `docs/superpowers/specs/2026-06-02-honcho-api-proxy-design.md` + +**Baseline gate (run before starting AND after every task):** +`make ci-web` (lint + typecheck + test + build). Targeted test during a task: `pnpm --filter @openconcho/web exec vitest run `. + +**Commit discipline:** conventional commits, one logical change per commit, body lines ≤100 chars, no AI attribution. Branch `feat/web-api-proxy` (already created; spec already committed there). + +> **Amendment (2026-06-02, post-execution):** Tasks 1–3 are implemented and committed +> (`d4452ab`, `9945e4c`, `0935099`). During execution a design correction was made: +> the web-mode base is **absolute same-origin** (`${location.origin}/api`), not the bare +> relative `"/api"`. Reason: a relative base makes `openapi-fetch` call +> `new Request("/api/...")`, which throws `ERR_INVALID_URL` under node/undici and is +> fragile in the browser. `dispatchFor` now returns `${location.origin}${API_PREFIX}` +> in web mode. Consequently `fleet.test.tsx` **was** updated (the original claim that it +> "stays green untouched" was wrong — the transport contract genuinely changed) and any +> test that inspects the dispatched URL asserts the absolute same-origin form. Tests mock +> `@/lib/http` (not `globalThis.fetch`), because `httpFetch` captures the fetch reference +> at module load, so `vi.stubGlobal("fetch", …)` would not intercept it. + +--- + +## File Structure + +| File | Responsibility | Action | +|------|----------------|--------| +| `packages/web/src/lib/platform.ts` | Single `isTauri()` predicate (leaf module, no app imports) | Create | +| `packages/web/src/lib/dispatch.ts` | `dispatchFor()` + proxy constants — the one transport decision | Create | +| `packages/web/src/lib/http.ts` | Keep `httpFetch`; consume `isTauri` from platform | Modify | +| `packages/web/src/lib/discovery.ts` | Re-export `isTauri` from platform; route probe via `dispatchFor` | Modify | +| `packages/web/src/api/client.ts` | Active-instance client via `dispatchFor` | Modify | +| `packages/web/src/api/scopedClient.ts` | Scoped client (Fleet/seed-kits) via `dispatchFor` | Modify | +| `packages/web/src/lib/config.ts` | `checkConnection` via `dispatchFor` + proxy-reject handling | Modify | +| `packages/web/src/lib/runtimeConfig.ts` | Drop `same-origin` sentinel | Modify | +| `packages/web/vite.config.ts` | Dev `/api` proxy middleware mirroring nginx | Modify | +| `docker/nginx.conf.template` | Header-driven `^~ /api/` block (replaces `/v3` + `/health`) | Modify | +| `docker/40-openconcho-config.sh` | Render allowlist `map` for `$allow_upstream` | Modify | +| `docker-compose.yml` | Retire `HONCHO_UPSTREAM`; document allowlist env | Modify | +| `AGENTS.md`, `README.md` | Proxy contract + env vars | Modify | +| `packages/web/src/test/dispatch.test.ts` | Unit tests for `dispatchFor` | Create | +| `packages/web/src/test/check-connection.test.ts` | Unit tests for `checkConnection` proxy behavior | Create | + +--- + +## Task 1: Extract `isTauri()` into a leaf module + +Prevents an import cycle: `discovery.ts` will later import `dispatchFor`, and `dispatch.ts` needs `isTauri`. A leaf `platform.ts` breaks the cycle and gives one canonical predicate (WIOCHE). + +**Files:** +- Create: `packages/web/src/lib/platform.ts` +- Test: `packages/web/src/test/platform.test.ts` +- Modify: `packages/web/src/lib/http.ts`, `packages/web/src/lib/discovery.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/web/src/test/platform.test.ts`: +```ts +import { afterEach, describe, expect, it } from "vitest"; +import { isTauri } from "@/lib/platform"; + +describe("isTauri", () => { + afterEach(() => { + delete (window as unknown as Record).__TAURI_INTERNALS__; + }); + + it("returns false in a plain browser/jsdom environment", () => { + expect(isTauri()).toBe(false); + }); + + it("returns true when the Tauri internals global is present", () => { + (window as unknown as Record).__TAURI_INTERNALS__ = {}; + expect(isTauri()).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/platform.test.ts` +Expected: FAIL — cannot resolve `@/lib/platform`. + +- [ ] **Step 3: Create the module** + +`packages/web/src/lib/platform.ts`: +```ts +/** True when running inside the Tauri desktop shell (WebView with injected internals). */ +export function isTauri(): boolean { + return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; +} +``` + +- [ ] **Step 4: Point existing consumers at the canonical predicate** + +In `packages/web/src/lib/http.ts`, replace the inline const with the shared predicate (call it at module load to preserve current behavior): +```ts +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { isTauri } from "@/lib/platform"; + +// Route fetch through Rust (reqwest) when running in Tauri — bypasses WebView CORS enforcement. +// Falls back to native browser fetch during plain web dev. +export const httpFetch: typeof globalThis.fetch = isTauri() + ? (tauriFetch as typeof globalThis.fetch) + : globalThis.fetch; +``` + +In `packages/web/src/lib/discovery.ts`, delete the local `isTauri` function (lines 8-10) and re-export the canonical one so existing importers keep working. Add at the top, after the `httpFetch` import: +```ts +export { isTauri } from "@/lib/platform"; +``` + +- [ ] **Step 5: Run tests + lint + typecheck** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/platform.test.ts && make lint && make typecheck` +Expected: PASS; no type errors. + +- [ ] **Step 6: Commit** + +```bash +git add packages/web/src/lib/platform.ts packages/web/src/lib/http.ts \ + packages/web/src/lib/discovery.ts packages/web/src/test/platform.test.ts +git commit -m "refactor(web): extract isTauri into a leaf platform module" +``` + +--- + +## Task 2: `dispatchFor` helper + proxy constants + +The heart of the design. Decides transport per instance. + +**Files:** +- Create: `packages/web/src/lib/dispatch.ts` +- Test: `packages/web/src/test/dispatch.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/web/src/test/dispatch.test.ts`: +```ts +import { afterEach, describe, expect, it, vi } from "vitest"; + +const mockIsTauri = vi.fn(); +vi.mock("@/lib/platform", () => ({ isTauri: () => mockIsTauri() })); + +import { + API_PREFIX, + dispatchFor, + PROXY_REJECT_HEADER, + UPSTREAM_HEADER, +} from "@/lib/dispatch"; + +afterEach(() => mockIsTauri.mockReset()); + +describe("dispatchFor — web mode", () => { + it("targets the /api prefix and carries the upstream header", () => { + mockIsTauri.mockReturnValue(false); + const d = dispatchFor({ baseUrl: "https://honcho.example.net/", token: "" }); + expect(d.baseUrl).toBe(API_PREFIX); + expect(d.headers[UPSTREAM_HEADER]).toBe("https://honcho.example.net"); + expect(d.headers.Authorization).toBeUndefined(); + }); + + it("adds Authorization only when a token is present", () => { + mockIsTauri.mockReturnValue(false); + const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" }); + expect(d.headers.Authorization).toBe("Bearer sk-1"); + }); +}); + +describe("dispatchFor — tauri mode", () => { + it("targets the absolute URL with no upstream header", () => { + mockIsTauri.mockReturnValue(true); + const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" }); + expect(d.baseUrl).toBe("https://honcho.example.net"); + expect(d.headers[UPSTREAM_HEADER]).toBeUndefined(); + expect(d.headers.Authorization).toBe("Bearer sk-1"); + }); +}); + +describe("proxy reject header constant", () => { + it("is the agreed sentinel name", () => { + expect(PROXY_REJECT_HEADER).toBe("X-Honcho-Proxy-Reject"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/dispatch.test.ts` +Expected: FAIL — cannot resolve `@/lib/dispatch`. + +- [ ] **Step 3: Create the helper** + +`packages/web/src/lib/dispatch.ts`: +```ts +import { httpFetch } from "@/lib/http"; +import { isTauri } from "@/lib/platform"; + +/** Same-origin path prefix the web build issues all Honcho calls through. */ +export const API_PREFIX = "/api"; +/** Request header naming the real Honcho upstream for the proxy to forward to. */ +export const UPSTREAM_HEADER = "X-Honcho-Upstream"; +/** Response header the proxy sets on its OWN refusals (so they aren't read as upstream auth). */ +export const PROXY_REJECT_HEADER = "X-Honcho-Proxy-Reject"; + +export interface Dispatch { + baseUrl: string; + headers: Record; + fetch: typeof globalThis.fetch; +} + +function normalizeUpstream(url: string): string { + return url.trim().replace(/\/+$/, ""); +} + +/** + * Resolve how to issue a request for an instance. + * - Web: same-origin `/api` + `X-Honcho-Upstream` header (proxy forwards server-side, no CORS). + * - Tauri: the absolute instance URL via reqwest (no browser same-origin policy). + */ +export function dispatchFor(instance: { baseUrl: string; token?: string }): Dispatch { + const headers: Record = { "Content-Type": "application/json" }; + if (instance.token) headers.Authorization = `Bearer ${instance.token}`; + + if (isTauri()) { + return { baseUrl: instance.baseUrl, headers, fetch: httpFetch }; + } + + headers[UPSTREAM_HEADER] = normalizeUpstream(instance.baseUrl); + return { baseUrl: API_PREFIX, headers, fetch: httpFetch }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/dispatch.test.ts` +Expected: PASS (5 assertions across 4 tests). + +- [ ] **Step 5: Commit** + +```bash +git add packages/web/src/lib/dispatch.ts packages/web/src/test/dispatch.test.ts +git commit -m "feat(web): add dispatchFor transport helper for same-origin proxy" +``` + +--- + +## Task 3: Route the API clients through `dispatchFor` + +**Files:** +- Modify: `packages/web/src/api/client.ts`, `packages/web/src/api/scopedClient.ts` + +- [ ] **Step 1: Rewrite `client.ts`** + +`packages/web/src/api/client.ts`: +```ts +import createClient from "openapi-fetch"; +import { loadConfig } from "@/lib/config"; +import { dispatchFor } from "@/lib/dispatch"; +import type { paths } from "./schema.d.ts"; + +export function createHonchoClient() { + const config = loadConfig() ?? { baseUrl: "http://localhost:8000", token: "" }; + const { baseUrl, headers, fetch } = dispatchFor(config); + return createClient({ baseUrl, headers, fetch }); +} + +export const client = { + get current() { + return createHonchoClient(); + }, +}; +``` + +- [ ] **Step 2: Rewrite `scopedClient.ts`** + +`packages/web/src/api/scopedClient.ts`: +```ts +import createClient from "openapi-fetch"; +import type { Instance } from "@/lib/config"; +import { dispatchFor } from "@/lib/dispatch"; +import type { paths } from "./schema.d.ts"; + +export type ScopedClient = ReturnType>; + +/** + * Create an openapi-fetch client bound to a specific instance. Use for views that + * query non-active instances (e.g. the Fleet side-by-side comparison). Each scoped + * client self-routes via its own X-Honcho-Upstream header in web mode. + */ +export function createScopedClient(instance: Instance): ScopedClient { + const { baseUrl, headers, fetch } = dispatchFor(instance); + return createClient({ baseUrl, headers, fetch }); +} +``` + +- [ ] **Step 3: Verify the full web suite stays green** + +Run: `pnpm --filter @openconcho/web exec vitest run && make typecheck && make lint` +Expected: PASS — existing `fleet.test.tsx`, `seed-kits.test.ts`, `app.test.tsx` still pass (transport swap is invisible to them). + +- [ ] **Step 4: Commit** + +```bash +git add packages/web/src/api/client.ts packages/web/src/api/scopedClient.ts +git commit -m "feat(web): route api clients through dispatchFor" +``` + +--- + +## Task 4: `checkConnection` + discovery via the proxy, with reject handling + +**Files:** +- Modify: `packages/web/src/lib/config.ts` (`checkConnection`), `packages/web/src/lib/discovery.ts` (`suggestNameForInstance`) +- Test: `packages/web/src/test/check-connection.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/web/src/test/check-connection.test.ts`: +```ts +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock the platform predicate (web mode) and the fetch boundary. We mock +// @/lib/http — NOT globalThis.fetch — because httpFetch captures the fetch +// reference at module load, so vi.stubGlobal would not be observed by dispatchFor. +const { mockIsTauri, httpFetchMock } = vi.hoisted(() => ({ + mockIsTauri: vi.fn(() => false), + httpFetchMock: vi.fn(), +})); +vi.mock("@/lib/platform", () => ({ isTauri: () => mockIsTauri() })); +vi.mock("@/lib/http", () => ({ httpFetch: httpFetchMock })); + +import { checkConnection } from "@/lib/config"; + +afterEach(() => { + httpFetchMock.mockReset(); + mockIsTauri.mockReturnValue(false); +}); + +describe("checkConnection — web proxy mode", () => { + it("calls the absolute same-origin /api path with the upstream header", async () => { + httpFetchMock.mockResolvedValue(new Response("{}", { status: 200 })); + const res = await checkConnection("https://honcho.example.net", "sk-1"); + expect(res.status).toBe("ok"); + const [url, init] = httpFetchMock.mock.calls[0]; + expect(String(url)).toBe(`${location.origin}/api/v3/workspaces/list`); + expect((init.headers as Record)["X-Honcho-Upstream"]).toBe( + "https://honcho.example.net", + ); + expect((init.headers as Record).Authorization).toBe("Bearer sk-1"); + }); + + it("maps an upstream 401 to auth-required", async () => { + httpFetchMock.mockResolvedValue(new Response("{}", { status: 401 })); + const res = await checkConnection("https://honcho.example.net"); + expect(res.status).toBe("auth-required"); + }); + + it("treats a proxy reject as unreachable, not auth-required", async () => { + httpFetchMock.mockResolvedValue( + new Response("", { status: 403, headers: { "X-Honcho-Proxy-Reject": "allowlist" } }), + ); + const res = await checkConnection("https://blocked.example.net"); + expect(res.status).toBe("unreachable"); + expect(res.message).toMatch(/allowlist/i); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/check-connection.test.ts` +Expected: FAIL — current `checkConnection` fetches `${baseUrl}/v3/...` directly (no `/api`, no upstream header, no reject handling). + +- [ ] **Step 3: Rewrite `checkConnection`** + +In `packages/web/src/lib/config.ts`, replace the `checkConnection` body (keep the signature). Add the import `import { dispatchFor, PROXY_REJECT_HEADER } from "@/lib/dispatch";` at the top (and remove the now-unused `httpFetch` import if nothing else in the file uses it): +```ts +export async function checkConnection( + baseUrl: string, + token?: string, +): Promise<{ status: HealthStatus; message: string }> { + try { + const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl, token }); + const res = await fetch(`${base}/v3/workspaces/list`, { + method: "POST", + headers, + body: JSON.stringify({}), + signal: AbortSignal.timeout(5000), + }); + + const reject = res.headers.get(PROXY_REJECT_HEADER); + if (reject) { + return { status: "unreachable", message: `Proxy refused upstream (${reject})` }; + } + if (res.ok) return { status: "ok", message: "Connected successfully" }; + if (res.status === 401 || res.status === 403) { + return { status: "auth-required", message: "Authentication required — provide an API token" }; + } + return { status: "unreachable", message: `Server returned ${res.status}` }; + } catch (err) { + const msg = err instanceof Error ? err.message : "Unknown error"; + if (msg.includes("AbortError") || msg.includes("timeout")) { + return { status: "unreachable", message: "Connection timed out" }; + } + return { status: "unreachable", message: `Cannot reach server: ${msg}` }; + } +} +``` + +- [ ] **Step 4: Route the discovery probe through `dispatchFor` too** + +In `packages/web/src/lib/discovery.ts`, rewrite `suggestNameForInstance` to dispatch consistently. Add `import { dispatchFor } from "@/lib/dispatch";` and replace the fetch line: +```ts +export async function suggestNameForInstance(baseUrl: string): Promise { + try { + const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl }); + const res = await fetch(`${base}/v3/workspaces/list?page=1&page_size=1`, { + method: "POST", + headers, + body: JSON.stringify({}), + signal: AbortSignal.timeout(2000), + }); + if (!res.ok) return null; + const data = (await res.json()) as { items?: Array<{ id?: string }> }; + const wsId = data.items?.[0]?.id; + if (typeof wsId === "string" && wsId.length > 0) { + return deriveNameFromWorkspaceId(wsId); + } + return null; + } catch { + return null; + } +} +``` +(Leave the top-of-file `httpFetch` import only if still referenced; otherwise remove it. `discovery.ts` no longer needs `httpFetch` after this change — remove the import.) + +- [ ] **Step 5: Run tests + gate** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/check-connection.test.ts && pnpm --filter @openconcho/web exec vitest run && make typecheck && make lint` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add packages/web/src/lib/config.ts packages/web/src/lib/discovery.ts \ + packages/web/src/test/check-connection.test.ts +git commit -m "feat(web): route checkConnection and discovery through the proxy" +``` + +--- + +## Task 5: Drop the `same-origin` sentinel from runtime config + +In header mode the default instance needs a real absolute URL (it becomes the header value); `same-origin` was glue for the retired `/v3` proxy. + +**Files:** +- Modify: `packages/web/src/lib/runtimeConfig.ts` +- Test: `packages/web/src/test/runtime-config.test.ts` + +- [ ] **Step 1: Write the failing test** + +`packages/web/src/test/runtime-config.test.ts`: +```ts +import { afterEach, describe, expect, it } from "vitest"; +import { runtimeDefaultBaseUrl } from "@/lib/runtimeConfig"; + +const KEY = "__OPENCONCHO_DEFAULT_HONCHO_URL__"; + +afterEach(() => { + delete (globalThis as Record)[KEY]; +}); + +describe("runtimeDefaultBaseUrl", () => { + it("returns an injected absolute URL verbatim", () => { + (globalThis as Record)[KEY] = "https://honcho.example.net"; + expect(runtimeDefaultBaseUrl()).toBe("https://honcho.example.net"); + }); + + it("returns null when unset or empty", () => { + expect(runtimeDefaultBaseUrl()).toBeNull(); + (globalThis as Record)[KEY] = " "; + expect(runtimeDefaultBaseUrl()).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify current behavior is covered/fails appropriately** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/runtime-config.test.ts` +Expected: PASS for absolute/empty (existing behavior), but the goal is to simplify; proceed to remove the sentinel branch. + +- [ ] **Step 3: Simplify the module** + +`packages/web/src/lib/runtimeConfig.ts`: +```ts +const GLOBAL_KEY = "__OPENCONCHO_DEFAULT_HONCHO_URL__"; + +/** + * Runtime-injected default Honcho base URL for container deployments. + * + * The Docker image writes `/config.js` from `OPENCONCHO_DEFAULT_HONCHO_URL` at + * container start, so one prebuilt image can target any backend without a rebuild. + * The web build proxies this URL via the same-origin `/api` reverse proxy (no CORS). + * + * - an absolute URL → that URL (seeds the first instance) + * - empty / unset → null (no default; the user configures in Settings) + */ +export function runtimeDefaultBaseUrl(): string | null { + const raw = (globalThis as Record)[GLOBAL_KEY]; + if (typeof raw !== "string" || raw.trim() === "") return null; + return raw.trim(); +} +``` + +- [ ] **Step 4: Run test + gate** + +Run: `pnpm --filter @openconcho/web exec vitest run src/test/runtime-config.test.ts && make typecheck && make lint` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add packages/web/src/lib/runtimeConfig.ts packages/web/src/test/runtime-config.test.ts +git commit -m "refactor(web): drop same-origin sentinel from runtime config" +``` + +--- + +## Task 6: nginx header-driven `/api` proxy + +**Files:** +- Modify: `docker/nginx.conf.template` + +- [ ] **Step 1: Replace the `/v3` + `/health` blocks with the `/api` block** + +In `docker/nginx.conf.template`, delete the `location ^~ /v3/ { ... }` and `location = /health { ... }` blocks (lines 21-32) and the `set $honcho_upstream ...` line. Keep the `resolver` line. Insert: +```nginx + # Header-driven same-origin proxy: the browser names the Honcho upstream per + # request via X-Honcho-Upstream, so the browser never makes a cross-origin call. + # $allow_upstream is provided by the allowlist map in conf.d (entrypoint-rendered). + location ^~ /api/ { + set $upstream $http_x_honcho_upstream; + if ($upstream = "") { + add_header X-Honcho-Proxy-Reject "no-upstream" always; + return 421; + } + if ($allow_upstream = 0) { + add_header X-Honcho-Proxy-Reject "allowlist" always; + return 403; + } + rewrite ^/api/(.*)$ /$1 break; + proxy_pass $upstream; + proxy_ssl_server_name on; + proxy_set_header Host $proxy_host; + proxy_set_header X-Honcho-Upstream ""; + } +``` + +- [ ] **Step 2: Validate template renders to valid nginx syntax** + +Run (renders the template with a dummy upstream and an open allowlist map, then `nginx -t`): +```bash +docker run --rm -e HONCHO_UPSTREAM=http://x:8000 -v "$PWD/docker":/d nginxinc/nginx-unprivileged:stable sh -c ' + mkdir -p /etc/nginx/conf.d + echo "map \$http_x_honcho_upstream \$allow_upstream { default 1; }" > /etc/nginx/conf.d/allowlist_map.conf + envsubst "\$HONCHO_UPSTREAM" < /d/nginx.conf.template > /etc/nginx/conf.d/default.conf + nginx -t' +``` +Expected: `nginx: configuration file /etc/nginx/nginx.conf test is successful`. + +- [ ] **Step 3: Commit** + +```bash +git add docker/nginx.conf.template +git commit -m "feat(docker): header-driven /api reverse proxy in nginx" +``` + +--- + +## Task 7: Entrypoint renders the allowlist map + +**Files:** +- Modify: `docker/40-openconcho-config.sh` + +- [ ] **Step 1: Append allowlist-map rendering** + +In `docker/40-openconcho-config.sh`, after the existing `config.js` heredoc, append: +```sh +# Render the SSRF allowlist into an nginx map for $allow_upstream. +# Unset/empty OPENCONCHO_UPSTREAM_ALLOWLIST → open (default 1), fine for the +# localhost-bound default. Set it (comma-separated host globs) before exposing +# the proxy (e.g. behind a tunnel) to reject non-matching upstreams. +ALLOWLIST_CONF=/etc/nginx/conf.d/allowlist_map.conf +if [ -z "${OPENCONCHO_UPSTREAM_ALLOWLIST:-}" ]; then + printf 'map $http_x_honcho_upstream $allow_upstream { default 1; }\n' > "$ALLOWLIST_CONF" +else + { + printf 'map $http_x_honcho_upstream $allow_upstream {\n' + printf ' default 0;\n' + IFS=',' + for host in $OPENCONCHO_UPSTREAM_ALLOWLIST; do + host=$(printf '%s' "$host" | tr -d ' ') + [ -z "$host" ] && continue + esc=$(printf '%s' "$host" | sed -e 's/[.]/\\./g' -e 's/[*]/[^/]*/g') + printf ' "~^https?://%s(:[0-9]+)?(/.*)?$" 1;\n' "$esc" + done + printf '}\n' + } > "$ALLOWLIST_CONF" +fi +``` + +- [ ] **Step 2: Validate generated map syntax (allowlist set)** + +Run: +```bash +docker run --rm -e OPENCONCHO_UPSTREAM_ALLOWLIST="honcho.example.net,*.honcho.dev" \ + -e HONCHO_UPSTREAM=http://x:8000 -v "$PWD/docker":/d nginxinc/nginx-unprivileged:stable sh -c ' + mkdir -p /etc/nginx/conf.d + export OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net + sh /d/40-openconcho-config.sh || true + envsubst "\$HONCHO_UPSTREAM" < /d/nginx.conf.template > /etc/nginx/conf.d/default.conf + nginx -t && cat /etc/nginx/conf.d/allowlist_map.conf' +``` +Expected: `nginx -t` success; printed map contains regex lines for both hosts. +Note: the script writes `config.js` to `/usr/share/nginx/html`; if that dir is absent in this bare check, the heredoc line may error — that is fine for syntax validation (the `|| true` guards it). The allowlist block still runs. + +- [ ] **Step 3: Commit** + +```bash +git add docker/40-openconcho-config.sh +git commit -m "feat(docker): render SSRF allowlist map from env" +``` + +--- + +## Task 8: Vite dev proxy middleware (dev/CI parity) + +**Files:** +- Modify: `packages/web/vite.config.ts` + +- [ ] **Step 1: Add a `configureServer` plugin mirroring nginx** + +In `packages/web/vite.config.ts`, add this plugin factory above `defineConfig` and include `honchoApiProxy()` in the `plugins` array (after `react()`): +```ts +import type { Plugin } from "vite"; + +function honchoApiProxy(): Plugin { + const HEADER = "x-honcho-upstream"; + return { + name: "honcho-api-proxy", + configureServer(server) { + server.middlewares.use("/api", async (req, res) => { + const upstream = req.headers[HEADER]; + if (typeof upstream !== "string" || upstream.trim() === "") { + res.statusCode = 421; + res.setHeader("X-Honcho-Proxy-Reject", "no-upstream"); + res.end(); + return; + } + const target = upstream.replace(/\/+$/, "") + (req.url ?? ""); + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + try { + const upstreamRes = await fetch(target, { + method: req.method, + headers: { + "content-type": req.headers["content-type"] ?? "application/json", + ...(req.headers.authorization + ? { authorization: req.headers.authorization } + : {}), + }, + body: ["GET", "HEAD"].includes(req.method ?? "") ? undefined : Buffer.concat(chunks), + }); + res.statusCode = upstreamRes.status; + upstreamRes.headers.forEach((v, k) => res.setHeader(k, v)); + res.end(Buffer.from(await upstreamRes.arrayBuffer())); + } catch (e) { + res.statusCode = 502; + res.end(`proxy error: ${e instanceof Error ? e.message : String(e)}`); + } + }); + }, + }; +} +``` +Update the plugins line to: +```ts + plugins: [tanstackRouter({ autoCodeSplitting: true }), react(), honchoApiProxy(), tailwindcss()], +``` + +- [ ] **Step 2: Typecheck + lint (the config is type-checked by the build)** + +Run: `make typecheck && make lint` +Expected: PASS (no `any`, `Plugin` typed). + +- [ ] **Step 3: Commit** + +```bash +git add packages/web/vite.config.ts +git commit -m "feat(web): dev /api proxy middleware mirroring nginx" +``` + +--- + +## Task 9: Compose env + docs + +**Files:** +- Modify: `docker-compose.yml`, `AGENTS.md`, `README.md` + +- [ ] **Step 1: Update `docker-compose.yml`** + +Replace the `environment:` block and its comments so the upstream is no longer a single env var. New `environment:` section: +```yaml + environment: + # The SPA seeds its first instance from this absolute URL; the browser then + # routes all calls same-origin through /api, and nginx forwards them to the + # URL named per-request in the X-Honcho-Upstream header (no browser CORS). + OPENCONCHO_DEFAULT_HONCHO_URL: ${OPENCONCHO_DEFAULT_HONCHO_URL:-http://host.docker.internal:8000} + # Optional SSRF guard. Unset = forward anywhere (safe for the localhost-only + # binding below). Set comma-separated host globs before exposing the proxy: + # OPENCONCHO_UPSTREAM_ALLOWLIST: honcho.example.net,*.honcho.dev + OPENCONCHO_UPSTREAM_ALLOWLIST: ${OPENCONCHO_UPSTREAM_ALLOWLIST:-} +``` +Update the top-of-file comment block: replace the `HONCHO_UPSTREAM=...` example with `OPENCONCHO_DEFAULT_HONCHO_URL=https://honcho.example.net docker compose up` and note the per-request header model. + +- [ ] **Step 2: Update `AGENTS.md` Key Constraints** + +Replace the CORS-relevant bullet(s) with: +```markdown +- **Web CORS is handled by a same-origin `/api` proxy** — the browser issues all + Honcho calls to `/api/*` with an `X-Honcho-Upstream` header; nginx (docker) and a + Vite middleware (dev) forward server-side. Tauri bypasses CORS via reqwest and uses + absolute URLs. Optional `OPENCONCHO_UPSTREAM_ALLOWLIST` guards the proxy when exposed. +``` + +- [ ] **Step 3: Update `README.md`** + +In the Docker/run section, document `OPENCONCHO_DEFAULT_HONCHO_URL` (absolute URL seed) and `OPENCONCHO_UPSTREAM_ALLOWLIST` (optional, comma-separated host globs), and remove any `HONCHO_UPSTREAM` references. + +- [ ] **Step 4: Commit** + +```bash +git add docker-compose.yml AGENTS.md README.md +git commit -m "docs: document the /api proxy contract and env vars" +``` + +--- + +## Task 10: Final CI-parity gate + branch readiness + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full web gate** + +Run: `make ci-web` +Expected: lint, typecheck, test, and build all PASS. + +- [ ] **Step 2: Confirm no `HONCHO_UPSTREAM` or `same-origin` references remain** + +Run: `grep -rn "HONCHO_UPSTREAM\|same-origin" docker docker-compose.yml packages/web/src README.md AGENTS.md` +Expected: no matches in code/config (only the spec/plan under `docs/` may mention them historically). + +- [ ] **Step 3: Confirm clean tree** + +Run: `git status --short` +Expected: empty (all work committed). + +--- + +## Self-Review (completed by author) + +- **Spec coverage:** dispatch helper (T2/T3), checkConnection+discovery (T4), nginx header proxy + SNI + reject header (T6), allowlist map (T7), vite parity (T8), env migration + docs (T9), Fleet preserved (T3 — scopedClient routed; existing fleet.test.tsx asserted green), runtime sentinel drop (T5). All spec sections mapped. +- **Placeholder scan:** none — every code step shows full content. +- **Type consistency:** `dispatchFor`, `Dispatch`, `API_PREFIX`, `UPSTREAM_HEADER`, `PROXY_REJECT_HEADER` are defined in T2 and consumed verbatim in T3/T4. `isTauri()` defined T1, consumed T2. diff --git a/docs/superpowers/specs/2026-06-02-honcho-api-proxy-design.md b/docs/superpowers/specs/2026-06-02-honcho-api-proxy-design.md new file mode 100644 index 0000000..164abdc --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-honcho-api-proxy-design.md @@ -0,0 +1,243 @@ +# Header-Driven `/api` Proxy for Web CORS — Design + +- **Date:** 2026-06-02 +- **Status:** Approved (design) — pending spec review +- **Scope:** One concern — eliminate browser CORS for the web build by routing + Honcho API calls through a same-origin, header-driven reverse proxy. Preserve + existing Fleet aggregation. No new aggregation features (deferred). + +## Problem + +The web build (`@openconcho/web`) talks to Honcho directly from the browser. When +the configured instance URL is a different origin than the page (e.g. a self-hosted +Honcho at `https://honcho.example.net` while the UI runs on +`http://localhost:8080`), the browser issues a CORS preflight on the `Authorization` +header and the request fails — Honcho ships no `CORSMiddleware`. + +The desktop (Tauri) build is unaffected: it routes fetch through Rust/`reqwest` +(`packages/web/src/lib/http.ts`), which has no browser same-origin policy. + +The repo already had a partial mitigation (a static `^~ /v3/` nginx proxy keyed to a +single `HONCHO_UPSTREAM`), but it (a) supported only one backend and (b) was bypassed +the moment a user typed an absolute URL into Settings — which is the bug that surfaced. + +### Evidence gathered + +- Browser → `honcho.example.net` is reachable; the CORS error proves the + request reached Honcho and only the browser policy check failed. +- A Docker container under Colima **also** reaches the tailnet: `docker run ... + curl https://honcho.example.net/health` returned **HTTP 200**, connected over the + tailnet on `:443` with TLS verified. So a container-side proxy is viable on this + host (Colima forwards container egress through the host's tailnet routing). + +## Decisions + +1. **Coexist by runtime mode.** Tauri keeps absolute-URL + `reqwest`. The web build + (docker **and** `make dev-web`) routes through a same-origin `/api` proxy. One + build; behavior chosen at runtime by `isTauri()`. +2. **Header-driven routing.** The browser names the target upstream per request via + an `X-Honcho-Upstream` header (sourced from the active/scoped instance's + `baseUrl`). The proxy is a stateless forwarder; the frontend stays the single + source of truth for instances. No server-side slug→upstream map. +3. **SSRF posture: optional allowlist, open by default.** Unset + `OPENCONCHO_UPSTREAM_ALLOWLIST` ⇒ forward anywhere (safe for the default + `127.0.0.1:8080` binding). Set it (host globs) before exposing the proxy (e.g. + behind `cloudflared`) to reject non-matching upstreams. +4. **Aggregation: preserve, don't extend.** The existing Fleet dashboard + (`compareQueries.ts`, `fleetAggregates.ts`, `FleetDashboard`/`FleetRow`) must keep + working identically. New cross-instance merge/dedup/search is explicitly a + **non-goal** of this PR. + +## Architecture + +``` +WEB (docker + dev): + browser ──same-origin──▶ /api/v3/... (openapi-fetch base = "/api") + X-Honcho-Upstream: https://honcho.example.net (from instance.baseUrl) + Authorization: Bearer … (unchanged, when set) + │ proxy: validate header, allowlist-check, strip "/api", + │ proxy_pass $upstream, set SNI/Host, drop routing header + ▼ + https://honcho.example.net/v3/... (server-side hop — no CORS) + +TAURI: + webview ──reqwest──▶ https://honcho.example.net/v3/... (unchanged) +``` + +**Why a custom header is free here:** `X-Honcho-Upstream` rides a *same-origin* +request (browser → `/api`), so it triggers no CORS preflight. Preflight only fires +cross-origin — the exact condition this design removes. + +**Why the instance store is unchanged:** instances still persist an absolute +`baseUrl` (`z.string().url()` stays valid). We change only *how a request is +dispatched*, not what is stored. In web mode the instance URL stops being the fetch +target and becomes the header value. + +## Components + +### A. Centralized dispatch helper (new) — `src/lib/dispatch.ts` + +Single source of truth for "how to issue a request for an instance," replacing four +ad-hoc constructions. + +```ts +export const API_PREFIX = "/api"; +export const UPSTREAM_HEADER = "X-Honcho-Upstream"; + +export interface Dispatch { + baseUrl: string; // "/api" (web) | instance.baseUrl (tauri) + headers: Record; // Content-Type, Authorization?, X-Honcho-Upstream? + fetch: typeof globalThis.fetch; // globalThis.fetch (web) | tauriFetch (tauri) +} + +export function dispatchFor( + instance: Pick, +): Dispatch; +``` + +- **Web:** `baseUrl = API_PREFIX`; headers include `UPSTREAM_HEADER = + normalizedUpstream(instance.baseUrl)` (trailing slash stripped) and `Authorization` + when `token` is non-empty; `fetch = globalThis.fetch`. +- **Tauri:** `baseUrl = instance.baseUrl`; no upstream header; `fetch = tauriFetch`. + +`API_PREFIX` and `UPSTREAM_HEADER` are named constants (WIOCHE) referenced by both +the frontend and documented for the proxy. + +### B. Consumers of the helper (the four dispatch sites) + +| Site | File | Change | +|------|------|--------| +| Active-instance client | `src/api/client.ts` | build client from `dispatchFor(loadConfig())` | +| Scoped client (Fleet/compare, seed-kits) | `src/api/scopedClient.ts` | build client from `dispatchFor(instance)` | +| Connection health check | `src/lib/config.ts` `checkConnection` | fetch `${baseUrl}/v3/workspaces/list` via `dispatchFor` (hits `/api/...` in web) | +| Discovery name probe | `src/lib/discovery.ts` `suggestNameForInstance` | same, via `dispatchFor` | + +`compareQueries.ts` and `fleetAggregates.ts` need **no changes** — they go through +`createScopedClient`, so the transport swap is invisible to them. `FleetRow.tsx:114` +uses `instance.baseUrl` only to render a hostname label (cosmetic) — untouched. + +### C. nginx proxy — `docker/nginx.conf.template` + +Replace the `^~ /v3/` and `= /health` upstream blocks with one header-driven block: + +```nginx +resolver 127.0.0.11 ipv6=off valid=10s; + +# Rendered by the entrypoint from OPENCONCHO_UPSTREAM_ALLOWLIST. +# Unset → default 1 (open). Set → 1 only for matching hosts, else 0. +# (map block injected here) + +location ^~ /api/ { + set $upstream $http_x_honcho_upstream; + if ($upstream = "") { return 421; } # misdirected: no target named + if ($allow_upstream = 0) { return 403; } # allowlist reject + rewrite ^/api/(.*)$ /$1 break; # strip /api, keep /v3/... + proxy_pass $upstream; + proxy_ssl_server_name on; # SNI for HTTPS upstreams + proxy_set_header Host $proxy_host; # upstream host, not localhost:8080 + proxy_set_header X-Honcho-Upstream ""; # never leak routing header upstream +} +``` + +`Authorization` and other client headers pass through by nginx default. The +container's own `/healthz` liveness endpoint is unchanged. + +### D. Vite dev parity — `vite.config.ts` + +A `configureServer` middleware mirrors nginx for `make dev-web`: read +`X-Honcho-Upstream`, forward `/api/*` (prefix stripped) to it, drop the routing +header, apply the same allowlist if configured. Result: dev behaves identically to +the docker image (local/CI parity). + +### E. Config / env — `docker/40-openconcho-config.sh`, `docker-compose.yml` + +- `OPENCONCHO_DEFAULT_HONCHO_URL` keeps seeding the first instance, now an **absolute + URL only**. Drop the `same-origin` sentinel (glue for the retired `/v3` proxy) from + `src/lib/runtimeConfig.ts`. +- **Retire `HONCHO_UPSTREAM`** from compose — the upstream now comes from the header. +- New optional `OPENCONCHO_UPSTREAM_ALLOWLIST` (comma-separated host globs, e.g. + `honcho.example.net,*.honcho.dev`). The entrypoint renders it into the + nginx `map` for `$allow_upstream`; unset ⇒ map default 1. + +## Data flow (Fleet aggregation, web mode) + +``` +FleetDashboard → useScoped*(instanceA) → createScopedClient(A) → dispatchFor(A) + POST /api/v3/... X-Honcho-Upstream: A ─┐ + → useScoped*(instanceB) → createScopedClient(B) → dispatchFor(B) ├▶ nginx → A,B + POST /api/v3/... X-Honcho-Upstream: B ─┘ (concurrent) +fleetAggregates.ts merges results (transport-agnostic). B down ⇒ only B's column errors. +``` + +Query keys in `compareQueries.ts` are already scoped by `instance.id`, so caches +never collide across columns. Stateless per-request routing isolates partial +failures per instance. + +## Error handling + +The proxy must not let its own refusals masquerade as upstream responses. Both +proxy-origin refusals carry a sentinel response header **`X-Honcho-Proxy-Reject`** +(value: `no-upstream` | `allowlist`). `checkConnection` treats any response bearing +that header as `unreachable` with the reject reason — **regardless of status code** — +so an allowlist `403` is never mis-mapped to the upstream's auth `403`. + +- **No `X-Honcho-Upstream`** (misconfigured web request) → proxy `421` + + `X-Honcho-Proxy-Reject: no-upstream`. Fail loud, not silent. +- **Allowlist reject** → proxy `403` + `X-Honcho-Proxy-Reject: allowlist`. The + sentinel header is what disambiguates it from the upstream's own `401/403` (which + arrive without the header and still map to `auth-required`). +- **Upstream unreachable / TLS failure** → nginx `502`; UI shows "Cannot reach + server." This is the symptom if a future host's container is *not* on the tailnet — + a network problem, not CORS (documented caveat: proxy portability depends on the + container host being able to route to the upstream). + +## Testing (TDD) + +Unit (mock only `isTauri` + the fetch boundary, per project test rules): + +1. `dispatchFor` (web): `baseUrl === "/api"`, sets `X-Honcho-Upstream` to the + normalized instance URL, sets `Authorization` only when token non-empty, uses + `globalThis.fetch`. +2. `dispatchFor` (tauri): `baseUrl === instance.baseUrl`, no upstream header, uses + `tauriFetch`. +3. `checkConnection` (web): issues to `/api/v3/workspaces/list` with the upstream + header; maps `ok` / `401|403` / other correctly. +4. **Aggregation regression:** two scoped clients for distinct instances emit two + distinct `X-Honcho-Upstream` values; existing `src/test/fleet.test.tsx` stays + green. + +Integration (nginx): compose up against a stub upstream — assert (a) header → +forward with `/api` stripped, (b) missing header → 421, (c) allowlist miss → 403, +(d) `X-Honcho-Upstream` absent on the upstream-side request. + +## Non-goals (deferred to separate PRs) + +- New cross-instance aggregation intelligence (unified merged lists, dedup, conflict + resolution, cross-instance search). +- Running Tailscale inside the Colima VM / any host-networking change (not needed — + reachability confirmed on the target host). +- Adding `CORSMiddleware` to Honcho (the proxy keeps openconcho self-contained and + backend-agnostic; this is the deliberate alternative *not* taken). + +## Files touched + +- `packages/web/src/lib/dispatch.ts` (new) +- `packages/web/src/api/client.ts` +- `packages/web/src/api/scopedClient.ts` +- `packages/web/src/lib/config.ts` +- `packages/web/src/lib/discovery.ts` +- `packages/web/src/lib/runtimeConfig.ts` +- `packages/web/vite.config.ts` +- `docker/nginx.conf.template` +- `docker/40-openconcho-config.sh` +- `docker-compose.yml` +- tests: `packages/web/src/test/` (new dispatch + checkConnection cases; keep + `fleet.test.tsx`, `settings-form.test.tsx` green) +- docs: `AGENTS.md`, `README` (env vars + the proxy contract) + +## Constraints honored + +- Web-only change → PR CI (web checks) covers it; no desktop `cargo-check` needed + (Tauri path unchanged). +- No hardcoded URLs (upstream comes from instance config / runtime env). +- One concern per PR (proxy only); conventional commits; push under `offendingcommit`. diff --git a/package.json b/package.json index 5833617..2b1a904 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openconcho", "private": true, - "version": "0.13.1", + "version": "0.15.0", "packageManager": "pnpm@10.33.2", "engines": { "node": ">=22", @@ -27,17 +27,17 @@ }, "devDependencies": { "@biomejs/biome": "catalog:", - "@commitlint/cli": "~20.5.2", - "@commitlint/config-conventional": "~20.5.0", - "@semantic-release/changelog": "^6.0.0", - "@semantic-release/commit-analyzer": "^13.0.0", + "@commitlint/cli": "~20.5.3", + "@commitlint/config-conventional": "~20.5.3", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/exec": "^7.1.0", - "@semantic-release/git": "^10.0.0", - "@semantic-release/github": "^10.0.0", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^10.3.5", "@semantic-release/npm": "^13.1.5", - "@semantic-release/release-notes-generator": "^14.0.0", + "@semantic-release/release-notes-generator": "^14.1.1", "husky": "~9.1.7", "semantic-release": "catalog:", - "turbo": "^2" + "turbo": "^2.9.16" } } diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 5503060..dcd6a4b 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -78,9 +78,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -117,9 +117,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ "serde_core", ] @@ -144,9 +144,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -155,19 +155,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] -name = "bumpalo" -version = "3.20.2" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -196,7 +205,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cairo-sys-rs", "glib", "libc", @@ -259,9 +268,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -348,12 +357,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -415,7 +418,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types", @@ -428,7 +431,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "libc", ] @@ -482,23 +485,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - [[package]] name = "cssparser" version = "0.36.0" @@ -508,7 +494,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -524,14 +510,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.23.0" @@ -572,6 +564,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "deranged" version = "0.5.8" @@ -582,19 +585,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -653,7 +643,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -661,9 +651,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -718,12 +708,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", + "cssparser", "foldhash 0.2.0", - "html5ever 0.38.0", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", + "selectors", + "tendril", ] [[package]] @@ -750,6 +740,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -764,14 +769,14 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg", ] @@ -913,16 +918,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -1011,15 +1006,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1129,17 +1115,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1149,7 +1124,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1218,7 +1193,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "futures-channel", "futures-core", "futures-executor", @@ -1330,9 +1305,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1370,9 +1345,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1392,18 +1367,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - [[package]] name = "html5ever" version = "0.38.0" @@ -1411,14 +1374,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", + "markup5ever", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1455,9 +1418,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1546,7 +1509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1656,9 +1619,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1682,7 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1702,16 +1665,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -1806,9 +1759,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1844,23 +1797,11 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "serde", "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.14.0", - "selectors 0.24.0", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1897,6 +1838,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1909,9 +1859,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -1939,9 +1889,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru-slab" @@ -1949,26 +1899,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", -] - [[package]] name = "markup5ever" version = "0.38.0" @@ -1976,32 +1906,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril 0.5.0", + "tendril", "web_atoms", ] -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -2030,20 +1943,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] [[package]] name = "muda" -version = "0.17.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" dependencies = [ "crossbeam-channel", "dpi", @@ -2054,10 +1967,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2066,7 +1979,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2075,12 +1988,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2096,17 +2003,11 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2155,20 +2056,41 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-core-foundation", "objc2-foundation", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", ] @@ -2179,13 +2101,45 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", "objc2-core-foundation", "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2207,7 +2161,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -2220,7 +2174,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", ] @@ -2231,7 +2185,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2243,9 +2197,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", "objc2-foundation", ] @@ -2255,7 +2228,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-app-kit", @@ -2271,9 +2244,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.4" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -2283,7 +2256,7 @@ dependencies = [ [[package]] name = "openconcho" -version = "0.13.1" +version = "0.15.0" dependencies = [ "futures", "serde", @@ -2382,105 +2355,25 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.6", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", + "phf_generator", + "phf_shared", ] [[package]] @@ -2490,34 +2383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -2526,47 +2392,20 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.2", -] - [[package]] name = "phf_shared" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -2583,9 +2422,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", @@ -2607,6 +2446,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.12.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2673,7 +2525,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -2700,12 +2552,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2733,9 +2579,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -2769,7 +2615,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.4", + "rand", "ring", "rustc-hash", "rustls", @@ -2816,59 +2662,14 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -2878,25 +2679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -2908,24 +2691,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2938,7 +2703,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -3046,9 +2811,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -3119,9 +2884,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", @@ -3230,40 +2995,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] - [[package]] name = "selectors" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.1", - "cssparser 0.36.0", - "derive_more 2.1.1", + "bitflags 2.12.1", + "cssparser", + "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", - "phf_codegen 0.13.1", + "phf", + "phf_codegen", "precomputed-hash", "rustc-hash", - "servo_arc 0.4.3", + "servo_arc", "smallvec", ] @@ -3332,9 +3079,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3386,11 +3133,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -3405,9 +3153,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling", "proc-macro2", @@ -3437,16 +3185,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - [[package]] name = "servo_arc" version = "0.4.3" @@ -3480,9 +3218,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "sigchld" @@ -3523,15 +3261,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3547,9 +3279,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3609,19 +3341,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -3630,30 +3349,18 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - [[package]] name = "string_cache_codegen" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -3688,7 +3395,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -3729,7 +3435,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3759,15 +3465,16 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.8" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", @@ -3778,13 +3485,14 @@ dependencies = [ "libc", "log", "ndk", - "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", @@ -3814,9 +3522,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" dependencies = [ "anyhow", "bytes", @@ -3842,7 +3550,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.2", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3865,9 +3573,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", @@ -3881,22 +3589,21 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -3914,9 +3621,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3928,9 +3635,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" dependencies = [ "anyhow", "glob", @@ -3939,7 +3646,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.12+spec-1.1.0", "walkdir", ] @@ -3966,9 +3672,9 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" dependencies = [ "anyhow", "dunce", @@ -3984,15 +3690,15 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-http" -version = "2.5.8" +version = "2.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfba7d4ec72763f9d1fdf73c217747f01e2c84b08b87a8cacd2f94f35853f84d" +checksum = "b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067" dependencies = [ "bytes", "cookie_store", @@ -4035,9 +3741,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" dependencies = [ "cookie", "dpi", @@ -4060,9 +3766,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", @@ -4086,24 +3792,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever 0.29.1", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf", + "plist", "proc-macro2", "quote", "regex", @@ -4115,7 +3821,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4124,24 +3830,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -4261,9 +3956,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -4335,6 +4030,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -4388,14 +4098,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -4404,7 +4114,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -4430,20 +4140,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4491,9 +4201,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", @@ -4505,10 +4215,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4525,9 +4235,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unic-char-property" @@ -4578,9 +4288,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -4633,9 +4343,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4694,12 +4404,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4726,9 +4430,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -4739,9 +4443,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -4749,9 +4453,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4759,9 +4463,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -4772,9 +4476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -4820,7 +4524,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -4828,9 +4532,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -4852,10 +4556,10 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -5420,9 +5124,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -5501,7 +5205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap 2.14.0", "log", "serde", @@ -5539,9 +5243,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.54.4" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", @@ -5627,18 +5331,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -5647,9 +5351,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 9bc728b..8d0a923 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openconcho" -version = "0.13.1" +version = "0.15.0" edition = "2021" [lib] diff --git a/packages/web/src/api/client.ts b/packages/web/src/api/client.ts index 7652996..514b426 100644 --- a/packages/web/src/api/client.ts +++ b/packages/web/src/api/client.ts @@ -1,21 +1,12 @@ import createClient from "openapi-fetch"; import { loadConfig } from "@/lib/config"; -import { httpFetch } from "@/lib/http"; +import { dispatchFor } from "@/lib/dispatch"; import type { paths } from "./schema.d.ts"; export function createHonchoClient() { - const config = loadConfig(); - const baseUrl = config?.baseUrl ?? "http://localhost:8000"; - const token = config?.token ?? ""; - - const headers: Record = { - "Content-Type": "application/json", - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - return createClient({ baseUrl, headers, fetch: httpFetch }); + const config = loadConfig() ?? { baseUrl: "http://localhost:8000", token: "" }; + const { baseUrl, headers, fetch } = dispatchFor(config); + return createClient({ baseUrl, headers, fetch }); } export const client = { diff --git a/packages/web/src/api/scopedClient.ts b/packages/web/src/api/scopedClient.ts index c37a9af..44c8764 100644 --- a/packages/web/src/api/scopedClient.ts +++ b/packages/web/src/api/scopedClient.ts @@ -1,18 +1,16 @@ import createClient from "openapi-fetch"; import type { Instance } from "@/lib/config"; -import { httpFetch } from "@/lib/http"; +import { dispatchFor } from "@/lib/dispatch"; import type { paths } from "./schema.d.ts"; export type ScopedClient = ReturnType>; /** - * Create an openapi-fetch client bound to a specific instance. Use for views - * that need to query non-active instances (e.g. side-by-side comparison). - * For single-instance access, prefer `client.current` which tracks the active - * instance via localStorage. + * Create an openapi-fetch client bound to a specific instance. Use for views that + * query non-active instances (e.g. the Fleet side-by-side comparison). Each scoped + * client self-routes via its own X-Honcho-Upstream header in web mode. */ export function createScopedClient(instance: Instance): ScopedClient { - const headers: Record = { "Content-Type": "application/json" }; - if (instance.token) headers.Authorization = `Bearer ${instance.token}`; - return createClient({ baseUrl: instance.baseUrl, headers, fetch: httpFetch }); + const { baseUrl, headers, fetch } = dispatchFor(instance); + return createClient({ baseUrl, headers, fetch }); } diff --git a/packages/web/src/lib/config.ts b/packages/web/src/lib/config.ts index 6dda1dd..faa9b10 100644 --- a/packages/web/src/lib/config.ts +++ b/packages/web/src/lib/config.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { httpFetch } from "@/lib/http"; +import { dispatchFor, PROXY_REJECT_HEADER } from "@/lib/dispatch"; import { runtimeDefaultBaseUrl } from "@/lib/runtimeConfig"; const LEGACY_KEY = "openconcho:config"; @@ -7,6 +7,13 @@ const STORE_KEY = "openconcho:instances"; export const HONCHO_CLOUD_URL = "https://api.honcho.dev"; +/** + * Connection-test timeout. Generous because a cold/idle self-hosted Honcho (DB + * pool spin-up, tunnel wake) can take several seconds on its first request — a + * tight 5s budget reported live-and-reachable instances as "Connection timed out". + */ +export const CONNECTION_TIMEOUT_MS = 15_000; + function normalizeBaseUrl(url: string): string { return url.trim().replace(/\/+$/, "").toLowerCase(); } @@ -16,7 +23,7 @@ export function isCloudInstance(instance: Pick): boolean { } export const configSchema = z.object({ - baseUrl: z.string().url({ message: "Must be a valid URL" }), + baseUrl: z.url({ message: "Must be a valid URL" }), token: z.string().optional().default(""), }); @@ -25,7 +32,7 @@ export type Config = z.infer; export const instanceSchema = z.object({ id: z.string().min(1), name: z.string().min(1, { message: "Name is required" }), - baseUrl: z.string().url({ message: "Must be a valid URL" }), + baseUrl: z.url({ message: "Must be a valid URL" }), token: z.string().optional().default(""), }); @@ -162,21 +169,21 @@ export type HealthStatus = "ok" | "auth-required" | "unreachable" | "checking"; export async function checkConnection( baseUrl: string, token?: string, -): Promise<{ - status: HealthStatus; - message: string; -}> { + timeoutMs: number = CONNECTION_TIMEOUT_MS, +): Promise<{ status: HealthStatus; message: string }> { try { - const headers: Record = { "Content-Type": "application/json" }; - if (token) headers.Authorization = `Bearer ${token}`; - - const res = await httpFetch(`${baseUrl}/v3/workspaces/list`, { + const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl, token }); + const res = await fetch(`${base}/v3/workspaces/list`, { method: "POST", headers, body: JSON.stringify({}), - signal: AbortSignal.timeout(5000), + signal: AbortSignal.timeout(timeoutMs), }); + const reject = res.headers.get(PROXY_REJECT_HEADER); + if (reject) { + return { status: "unreachable", message: `Proxy refused upstream (${reject})` }; + } if (res.ok) return { status: "ok", message: "Connected successfully" }; if (res.status === 401 || res.status === 403) { return { status: "auth-required", message: "Authentication required — provide an API token" }; diff --git a/packages/web/src/lib/discovery.ts b/packages/web/src/lib/discovery.ts index 83bc365..6784f80 100644 --- a/packages/web/src/lib/discovery.ts +++ b/packages/web/src/lib/discovery.ts @@ -1,14 +1,13 @@ -import { httpFetch } from "@/lib/http"; +import { dispatchFor } from "@/lib/dispatch"; +import { isTauri } from "@/lib/platform"; + +export { isTauri } from "@/lib/platform"; export interface DiscoveredInstance { port: number; base_url: string; } -export function isTauri(): boolean { - return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; -} - /** * Probe localhost ports for running Honcho instances. Desktop-only — the web * build can't port-scan due to CORS, so this returns an empty list when not @@ -38,9 +37,10 @@ export function deriveNameFromWorkspaceId(workspaceId: string): string { */ export async function suggestNameForInstance(baseUrl: string): Promise { try { - const res = await httpFetch(`${baseUrl}/v3/workspaces/list?page=1&page_size=1`, { + const { baseUrl: base, headers, fetch } = dispatchFor({ baseUrl }); + const res = await fetch(`${base}/v3/workspaces/list?page=1&page_size=1`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers, body: JSON.stringify({}), signal: AbortSignal.timeout(2000), }); diff --git a/packages/web/src/lib/dispatch.ts b/packages/web/src/lib/dispatch.ts new file mode 100644 index 0000000..3701e18 --- /dev/null +++ b/packages/web/src/lib/dispatch.ts @@ -0,0 +1,47 @@ +import { httpFetch } from "@/lib/http"; +import { isTauri } from "@/lib/platform"; + +/** Same-origin path prefix the web build issues all Honcho calls through. */ +export const API_PREFIX = "/api"; +/** Request header naming the real Honcho upstream for the proxy to forward to. */ +export const UPSTREAM_HEADER = "X-Honcho-Upstream"; +/** Response header the proxy sets on its OWN refusals (so they aren't read as upstream auth). */ +export const PROXY_REJECT_HEADER = "X-Honcho-Proxy-Reject"; + +export interface Dispatch { + baseUrl: string; + headers: Record; + fetch: typeof globalThis.fetch; +} + +function normalizeUpstream(url: string): string { + return url.trim().replace(/\/+$/, ""); +} + +/** + * Absolute same-origin base for the web proxy. Absolute (origin + `/api`), not the + * bare relative `/api`, so openapi-fetch and node/undici can construct a Request + * without an ambient document base — and so it resolves identically in the browser, + * behind a tunnel, and under jsdom. + */ +function webApiBase(): string { + const origin = typeof location !== "undefined" ? location.origin : ""; + return `${origin}${API_PREFIX}`; +} + +/** + * Resolve how to issue a request for an instance. + * - Web: same-origin `/api` + `X-Honcho-Upstream` header (proxy forwards server-side, no CORS). + * - Tauri: the absolute instance URL via reqwest (no browser same-origin policy). + */ +export function dispatchFor(instance: { baseUrl: string; token?: string }): Dispatch { + const headers: Record = { "Content-Type": "application/json" }; + if (instance.token) headers.Authorization = `Bearer ${instance.token}`; + + if (isTauri()) { + return { baseUrl: instance.baseUrl, headers, fetch: httpFetch }; + } + + headers[UPSTREAM_HEADER] = normalizeUpstream(instance.baseUrl); + return { baseUrl: webApiBase(), headers, fetch: httpFetch }; +} diff --git a/packages/web/src/lib/http.ts b/packages/web/src/lib/http.ts index 96c325a..41ba6b8 100644 --- a/packages/web/src/lib/http.ts +++ b/packages/web/src/lib/http.ts @@ -1,12 +1,8 @@ import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import { isTauri } from "@/lib/platform"; // Route fetch through Rust (reqwest) when running in Tauri — bypasses WebView CORS enforcement. -// Falls back to native browser fetch during plain web dev (`pnpm dev:web`). -const isTauri = Boolean( - typeof window !== "undefined" && - (window as unknown as Record).__TAURI_INTERNALS__, -); - -export const httpFetch: typeof globalThis.fetch = isTauri +// Falls back to native browser fetch during plain web dev. +export const httpFetch: typeof globalThis.fetch = isTauri() ? (tauriFetch as typeof globalThis.fetch) : globalThis.fetch; diff --git a/packages/web/src/lib/platform.ts b/packages/web/src/lib/platform.ts new file mode 100644 index 0000000..6755142 --- /dev/null +++ b/packages/web/src/lib/platform.ts @@ -0,0 +1,4 @@ +/** True when running inside the Tauri desktop shell (WebView with injected internals). */ +export function isTauri(): boolean { + return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; +} diff --git a/packages/web/src/lib/runtimeConfig.ts b/packages/web/src/lib/runtimeConfig.ts index 235822b..c2990dd 100644 --- a/packages/web/src/lib/runtimeConfig.ts +++ b/packages/web/src/lib/runtimeConfig.ts @@ -1,24 +1,17 @@ const GLOBAL_KEY = "__OPENCONCHO_DEFAULT_HONCHO_URL__"; -const SAME_ORIGIN = "same-origin"; /** * Runtime-injected default Honcho base URL for container deployments. * - * The Docker image writes `/config.js` from the `OPENCONCHO_DEFAULT_HONCHO_URL` - * env at container start, so one prebuilt image can target any backend without - * a rebuild (Vite envs are baked at build time and can't do this). + * The Docker image writes `/config.js` from `OPENCONCHO_DEFAULT_HONCHO_URL` at + * container start, so one prebuilt image can target any backend without a rebuild. + * The web build proxies this URL via the same-origin `/api` reverse proxy (no CORS). * - * - `"same-origin"` → the page's own origin (pairs with the nginx `/v3` reverse - * proxy, so the browser makes same-origin requests and CORS never applies) - * - an absolute URL → that URL - * - empty / unset → `null` (no default; the user configures in Settings) + * - an absolute URL → that URL (seeds the first instance) + * - empty / unset → null (no default; the user configures in Settings) */ export function runtimeDefaultBaseUrl(): string | null { const raw = (globalThis as Record)[GLOBAL_KEY]; if (typeof raw !== "string" || raw.trim() === "") return null; - const value = raw.trim(); - if (value === SAME_ORIGIN) { - return typeof location !== "undefined" ? location.origin : null; - } - return value; + return raw.trim(); } diff --git a/packages/web/src/test/check-connection.test.ts b/packages/web/src/test/check-connection.test.ts new file mode 100644 index 0000000..f12f688 --- /dev/null +++ b/packages/web/src/test/check-connection.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock the platform predicate (web mode) and the fetch boundary. We mock +// @/lib/http — NOT globalThis.fetch — because httpFetch captures the fetch +// reference at module load, so vi.stubGlobal would not be observed by dispatchFor. +const { mockIsTauri, httpFetchMock } = vi.hoisted(() => ({ + mockIsTauri: vi.fn(() => false), + httpFetchMock: vi.fn(), +})); +vi.mock("@/lib/platform", () => ({ isTauri: () => mockIsTauri() })); +vi.mock("@/lib/http", () => ({ httpFetch: httpFetchMock })); + +import { checkConnection } from "@/lib/config"; + +afterEach(() => { + httpFetchMock.mockReset(); + mockIsTauri.mockReturnValue(false); +}); + +describe("checkConnection — web proxy mode", () => { + it("calls the absolute same-origin /api path with the upstream header", async () => { + httpFetchMock.mockResolvedValue(new Response("{}", { status: 200 })); + const res = await checkConnection("https://honcho.example.net", "sk-1"); + expect(res.status).toBe("ok"); + const [url, init] = httpFetchMock.mock.calls[0]; + expect(String(url)).toBe(`${location.origin}/api/v3/workspaces/list`); + expect((init.headers as Record)["X-Honcho-Upstream"]).toBe( + "https://honcho.example.net", + ); + expect((init.headers as Record).Authorization).toBe("Bearer sk-1"); + }); + + it("maps an upstream 401 to auth-required", async () => { + httpFetchMock.mockResolvedValue(new Response("{}", { status: 401 })); + const res = await checkConnection("https://honcho.example.net"); + expect(res.status).toBe("auth-required"); + }); + + it("treats a proxy reject as unreachable, not auth-required", async () => { + httpFetchMock.mockResolvedValue( + new Response("", { status: 403, headers: { "X-Honcho-Proxy-Reject": "allowlist" } }), + ); + const res = await checkConnection("https://blocked.example.net"); + expect(res.status).toBe("unreachable"); + expect(res.message).toMatch(/allowlist/i); + }); +}); + +describe("checkConnection — timeout budget", () => { + // A fetch that resolves after `ms`, but rejects early if the abort signal fires — + // mirrors how a real slow upstream interacts with AbortSignal.timeout. + function delayedFetch(ms: number) { + return (_url: string, init: { signal?: AbortSignal }) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve(new Response("{}", { status: 200 })), ms); + init.signal?.addEventListener("abort", () => { + clearTimeout(timer); + reject(new DOMException("The operation timed out", "TimeoutError")); + }); + }); + } + + it("reports unreachable when the upstream is slower than the timeout budget", async () => { + httpFetchMock.mockImplementation(delayedFetch(80)); + const res = await checkConnection("https://slow.example.net", undefined, 20); + expect(res.status).toBe("unreachable"); + }); + + it("succeeds when a slow upstream responds within the (cold-start) budget", async () => { + httpFetchMock.mockImplementation(delayedFetch(20)); + const res = await checkConnection("https://slow.example.net", undefined, 200); + expect(res.status).toBe("ok"); + }); +}); diff --git a/packages/web/src/test/dispatch.test.ts b/packages/web/src/test/dispatch.test.ts new file mode 100644 index 0000000..1fbfdc7 --- /dev/null +++ b/packages/web/src/test/dispatch.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { mockIsTauri } = vi.hoisted(() => ({ mockIsTauri: vi.fn() })); +vi.mock("@/lib/platform", () => ({ isTauri: () => mockIsTauri() })); + +import { API_PREFIX, dispatchFor, PROXY_REJECT_HEADER, UPSTREAM_HEADER } from "@/lib/dispatch"; + +afterEach(() => mockIsTauri.mockReset()); + +describe("dispatchFor — web mode", () => { + it("targets the absolute same-origin /api base and carries the upstream header", () => { + mockIsTauri.mockReturnValue(false); + const d = dispatchFor({ baseUrl: "https://honcho.example.net/", token: "" }); + expect(d.baseUrl).toBe(`${location.origin}${API_PREFIX}`); + expect(d.headers[UPSTREAM_HEADER]).toBe("https://honcho.example.net"); + expect(d.headers.Authorization).toBeUndefined(); + }); + + it("adds Authorization only when a token is present", () => { + mockIsTauri.mockReturnValue(false); + const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" }); + expect(d.headers.Authorization).toBe("Bearer sk-1"); + }); +}); + +describe("dispatchFor — tauri mode", () => { + it("targets the absolute URL with no upstream header", () => { + mockIsTauri.mockReturnValue(true); + const d = dispatchFor({ baseUrl: "https://honcho.example.net", token: "sk-1" }); + expect(d.baseUrl).toBe("https://honcho.example.net"); + expect(d.headers[UPSTREAM_HEADER]).toBeUndefined(); + expect(d.headers.Authorization).toBe("Bearer sk-1"); + }); +}); + +describe("proxy reject header constant", () => { + it("is the agreed sentinel name", () => { + expect(PROXY_REJECT_HEADER).toBe("X-Honcho-Proxy-Reject"); + }); +}); diff --git a/packages/web/src/test/fleet.test.tsx b/packages/web/src/test/fleet.test.tsx index 5d39309..d1bf000 100644 --- a/packages/web/src/test/fleet.test.tsx +++ b/packages/web/src/test/fleet.test.tsx @@ -103,23 +103,25 @@ describe("scoped option builders", () => { (httpFetch as ReturnType).mockClear(); }); - it("scopes queue status requests by instance baseUrl and token", async () => { + it("scopes queue status requests via the same-origin proxy, upstream header, and token", async () => { const { httpFetch } = await import("@/lib/http"); const opts = scopedQueueStatusOptions(neo, "ws-1"); await opts.queryFn(); const req = (httpFetch as ReturnType).mock.calls[0][0] as Request; - expect(req.url).toBe("http://localhost:8001/v3/workspaces/ws-1/queue/status"); + expect(req.url).toBe(`${location.origin}/api/v3/workspaces/ws-1/queue/status`); + expect(req.headers.get("X-Honcho-Upstream")).toBe("http://localhost:8001"); expect(req.headers.get("Authorization")).toBe("Bearer neo-token"); }); - it("scopes conclusions-count requests by instance baseUrl and token", async () => { + it("scopes conclusions-count requests via the same-origin proxy, upstream header, and token", async () => { const { httpFetch } = await import("@/lib/http"); const opts = scopedConclusionsCountOptions(iris, "ws-9"); await opts.queryFn(); const req = (httpFetch as ReturnType).mock.calls[0][0] as Request; - expect(req.url.startsWith("http://localhost:8002/v3/workspaces/ws-9/conclusions/list")).toBe( + expect(req.url.startsWith(`${location.origin}/api/v3/workspaces/ws-9/conclusions/list`)).toBe( true, ); + expect(req.headers.get("X-Honcho-Upstream")).toBe("http://localhost:8002"); expect(req.headers.get("Authorization")).toBe("Bearer iris-token"); }); diff --git a/packages/web/src/test/instances.test.ts b/packages/web/src/test/instances.test.ts new file mode 100644 index 0000000..9042e45 --- /dev/null +++ b/packages/web/src/test/instances.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + addInstance, + deleteInstance, + getActiveInstance, + loadConfig, + loadStore, + setActiveInstance, + updateInstance, +} from "@/lib/config"; + +const STORE_KEY = "openconcho:instances"; +const LEGACY_KEY = "openconcho:config"; + +beforeEach(() => localStorage.clear()); +afterEach(() => localStorage.clear()); + +describe("instance store — add + active selection", () => { + it("makes the first added instance active", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + expect(loadStore().activeId).toBe(a.id); + }); + + it("does not steal active focus when adding more instances", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + addInstance({ name: "B", baseUrl: "https://b.example.net", token: "" }); + expect(loadStore().activeId).toBe(a.id); + }); + + it("appends instances in insertion order", () => { + addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + addInstance({ name: "B", baseUrl: "https://b.example.net", token: "" }); + expect(loadStore().instances.map((i) => i.name)).toEqual(["A", "B"]); + }); +}); + +describe("instance store — switching active", () => { + it("switches the active instance", () => { + addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + const b = addInstance({ name: "B", baseUrl: "https://b.example.net", token: "" }); + setActiveInstance(b.id); + expect(getActiveInstance()?.id).toBe(b.id); + }); + + it("ignores an unknown id", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + setActiveInstance("does-not-exist"); + expect(getActiveInstance()?.id).toBe(a.id); + }); +}); + +describe("instance store — deletion", () => { + it("falls back to the first remaining when the active instance is deleted", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + const b = addInstance({ name: "B", baseUrl: "https://b.example.net", token: "" }); + setActiveInstance(b.id); + deleteInstance(b.id); + expect(loadStore().activeId).toBe(a.id); + }); + + it("leaves the active id unchanged when a non-active instance is deleted", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + const b = addInstance({ name: "B", baseUrl: "https://b.example.net", token: "" }); + deleteInstance(b.id); + expect(getActiveInstance()?.id).toBe(a.id); + }); + + it("clears the active id when the last instance is removed", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + deleteInstance(a.id); + expect(loadStore().activeId).toBeNull(); + }); + + it("returns null config once every instance is gone", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + deleteInstance(a.id); + expect(loadConfig()).toBeNull(); + }); +}); + +describe("instance store — update", () => { + it("patches the named fields", () => { + const a = addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + updateInstance(a.id, { name: "Renamed", token: "sk-1" }); + expect(loadStore().instances[0]).toMatchObject({ name: "Renamed", token: "sk-1" }); + }); + + it("no-ops on an unknown id", () => { + addInstance({ name: "A", baseUrl: "https://a.example.net", token: "" }); + updateInstance("nope", { name: "X" }); + expect(loadStore().instances[0].name).toBe("A"); + }); +}); + +describe("instance store — active config", () => { + it("reflects the active instance's url and token", () => { + addInstance({ name: "A", baseUrl: "https://a.example.net", token: "sk-a" }); + expect(loadConfig()).toEqual({ baseUrl: "https://a.example.net", token: "sk-a" }); + }); +}); + +describe("instance store — legacy migration", () => { + it("migrates the legacy single-config key into the instances store", () => { + localStorage.setItem( + LEGACY_KEY, + JSON.stringify({ baseUrl: "https://legacy.example.net", token: "sk-legacy" }), + ); + const store = loadStore(); + expect(store.instances[0]).toMatchObject({ + name: "Default", + baseUrl: "https://legacy.example.net", + token: "sk-legacy", + }); + }); + + it("removes the legacy key after migrating", () => { + localStorage.setItem( + LEGACY_KEY, + JSON.stringify({ baseUrl: "https://legacy.example.net", token: "" }), + ); + loadStore(); + expect(localStorage.getItem(LEGACY_KEY)).toBeNull(); + expect(localStorage.getItem(STORE_KEY)).toBeTruthy(); + }); +}); diff --git a/packages/web/src/test/platform.test.ts b/packages/web/src/test/platform.test.ts new file mode 100644 index 0000000..5be6805 --- /dev/null +++ b/packages/web/src/test/platform.test.ts @@ -0,0 +1,17 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { isTauri } from "@/lib/platform"; + +describe("isTauri", () => { + afterEach(() => { + delete (window as unknown as Record).__TAURI_INTERNALS__; + }); + + it("returns false in a plain browser/jsdom environment", () => { + expect(isTauri()).toBe(false); + }); + + it("returns true when the Tauri internals global is present", () => { + (window as unknown as Record).__TAURI_INTERNALS__ = {}; + expect(isTauri()).toBe(true); + }); +}); diff --git a/packages/web/src/test/runtime-config.test.ts b/packages/web/src/test/runtime-config.test.ts index a13e936..695ac8a 100644 --- a/packages/web/src/test/runtime-config.test.ts +++ b/packages/web/src/test/runtime-config.test.ts @@ -8,22 +8,14 @@ afterEach(() => { }); describe("runtimeDefaultBaseUrl", () => { - it("returns null when the global is unset", () => { - expect(runtimeDefaultBaseUrl()).toBeNull(); - }); - - it("returns null when the global is blank", () => { - (globalThis as Record)[KEY] = " "; - expect(runtimeDefaultBaseUrl()).toBeNull(); - }); - - it("returns an absolute URL verbatim", () => { + it("returns an injected absolute URL verbatim", () => { (globalThis as Record)[KEY] = "https://honcho.example.net"; expect(runtimeDefaultBaseUrl()).toBe("https://honcho.example.net"); }); - it("resolves 'same-origin' to the page origin", () => { - (globalThis as Record)[KEY] = "same-origin"; - expect(runtimeDefaultBaseUrl()).toBe(location.origin); + it("returns null when unset or empty", () => { + expect(runtimeDefaultBaseUrl()).toBeNull(); + (globalThis as Record)[KEY] = " "; + expect(runtimeDefaultBaseUrl()).toBeNull(); }); }); diff --git a/packages/web/src/test/security.test.ts b/packages/web/src/test/security.test.ts index 7fc40c1..dcd551c 100644 --- a/packages/web/src/test/security.test.ts +++ b/packages/web/src/test/security.test.ts @@ -27,11 +27,11 @@ describe("security URL helpers", () => { expect(isSecureTokenTransport("http://localhost:8000")).toBe(true); expect(isSecureTokenTransport("http://127.0.0.1:8000")).toBe(true); expect(isSecureTokenTransport("http://192.168.1.50:8000")).toBe(false); - expect(isSecureTokenTransport("http://100.67.206.76:8000")).toBe(false); + expect(isSecureTokenTransport("http://192.0.2.10:8000")).toBe(false); }); it("returns a user-facing error for insecure token transport", () => { - expect(tokenTransportError("http://100.67.206.76:8000")).toMatch(/HTTPS/); + expect(tokenTransportError("http://192.0.2.10:8000")).toMatch(/HTTPS/); expect(tokenTransportError("https://honcho.example.com")).toBeNull(); }); }); diff --git a/packages/web/src/test/settings-form.test.tsx b/packages/web/src/test/settings-form.test.tsx index 3ea55f3..8c33cc3 100644 --- a/packages/web/src/test/settings-form.test.tsx +++ b/packages/web/src/test/settings-form.test.tsx @@ -58,7 +58,7 @@ describe("SettingsForm — self-hosted preset", () => { renderForm(); const baseUrl = screen.getByPlaceholderText("http://localhost:8000"); await user.clear(baseUrl); - await user.type(baseUrl, "http://100.67.206.76:8000"); + await user.type(baseUrl, "http://192.0.2.10:8000"); await user.type( screen.getByPlaceholderText(/required only if your instance has auth enabled/i), "secret-token", diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 1267586..4622347 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -1,10 +1,10 @@ import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import tailwindcss from "@tailwindcss/vite"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; -import path from "path"; -import { fileURLToPath } from "url"; -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const host = process.env.TAURI_DEV_HOST; @@ -12,9 +12,80 @@ const { version } = JSON.parse( readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"), ) as { version: string }; +// Dev-mode mirror of the nginx /api reverse proxy: read X-Honcho-Upstream and +// forward /api/* there, so `make dev-web` behaves identically to the docker image +// (same-origin requests, no browser CORS). Connect strips the /api mount prefix, +// so req.url is already the upstream path (e.g. /v3/workspaces/list). +function honchoApiProxy(): Plugin { + const HEADER = "x-honcho-upstream"; + // Mirror nginx's allowlist (spec §D): unset/empty => open; otherwise only + // matching upstream hosts forward. Glob `*` -> any non-slash run, like nginx. + const raw = process.env.OPENCONCHO_UPSTREAM_ALLOWLIST?.trim(); + const allowlist: RegExp[] | null = raw + ? raw + .split(",") + .map((h) => h.trim()) + .filter(Boolean) + .map((host) => { + const esc = host.replace(/[.]/g, "\\.").replace(/[*]/g, "[^/]*"); + return new RegExp(`^https?://${esc}(:[0-9]+)?(/.*)?$`); + }) + : null; + return { + name: "honcho-api-proxy", + configureServer(server) { + server.middlewares.use("/api", async (req, res) => { + const upstream = req.headers[HEADER]; + if (typeof upstream !== "string" || upstream.trim() === "") { + res.statusCode = 421; + res.setHeader("X-Honcho-Proxy-Reject", "no-upstream"); + res.end(); + return; + } + if (allowlist && !allowlist.some((re) => re.test(upstream))) { + res.statusCode = 403; + res.setHeader("X-Honcho-Proxy-Reject", "allowlist"); + res.end(); + return; + } + const target = upstream.replace(/\/+$/, "") + (req.url ?? ""); + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + try { + const upstreamRes = await fetch(target, { + method: req.method, + headers: { + "content-type": req.headers["content-type"] ?? "application/json", + ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}), + }, + body: ["GET", "HEAD"].includes(req.method ?? "") ? undefined : Buffer.concat(chunks), + }); + res.statusCode = upstreamRes.status; + // undici's fetch auto-decompresses the body, so the original + // content-encoding/length no longer describe what we re-send — + // drop those (and hop-by-hop headers) to avoid ERR_CONTENT_DECODING_FAILED. + const SKIP = new Set([ + "content-encoding", + "content-length", + "transfer-encoding", + "connection", + ]); + upstreamRes.headers.forEach((v, k) => { + if (!SKIP.has(k.toLowerCase())) res.setHeader(k, v); + }); + res.end(Buffer.from(await upstreamRes.arrayBuffer())); + } catch (e) { + res.statusCode = 502; + res.end(`proxy error: ${e instanceof Error ? e.message : String(e)}`); + } + }); + }, + }; +} + export default defineConfig({ clearScreen: false, - plugins: [tanstackRouter({ autoCodeSplitting: true }), react(), tailwindcss()], + plugins: [tanstackRouter({ autoCodeSplitting: true }), react(), honchoApiProxy(), tailwindcss()], define: { __APP_VERSION__: JSON.stringify(version), }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4678022..2060c0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@biomejs/biome': - specifier: ^2.4.0 - version: 2.4.13 + specifier: ^2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.59.1 version: 1.59.1 @@ -31,11 +31,11 @@ catalogs: specifier: ^6.0.1 version: 6.0.1 '@vitest/coverage-v8': - specifier: ^4.0.0 - version: 4.1.7 + specifier: ^4.1.8 + version: 4.1.8 jsdom: - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^29.1.1 + version: 29.1.1 react: specifier: ^19.2.5 version: 19.2.5 @@ -43,7 +43,7 @@ catalogs: specifier: ^19.2.5 version: 19.2.5 semantic-release: - specifier: ^25.0.0 + specifier: ^25.0.3 version: 25.0.3 typescript: specifier: ~6.0.2 @@ -52,8 +52,8 @@ catalogs: specifier: ^8.0.10 version: 8.0.10 vitest: - specifier: ^4.0.0 - version: 4.1.5 + specifier: ^4.1.8 + version: 4.1.8 zod: specifier: ^4.0.0 version: 4.3.6 @@ -66,34 +66,34 @@ importers: devDependencies: '@biomejs/biome': specifier: 'catalog:' - version: 2.4.13 + version: 2.4.16 '@commitlint/cli': - specifier: ~20.5.2 - version: 20.5.2(@types/node@25.6.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3) + specifier: ~20.5.3 + version: 20.5.3(@types/node@25.6.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3) '@commitlint/config-conventional': - specifier: ~20.5.0 - version: 20.5.0 + specifier: ~20.5.3 + version: 20.5.3 '@semantic-release/changelog': - specifier: ^6.0.0 + specifier: ^6.0.3 version: 6.0.3(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/commit-analyzer': - specifier: ^13.0.0 + specifier: ^13.0.1 version: 13.0.1(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/exec': specifier: ^7.1.0 version: 7.1.0(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/git': - specifier: ^10.0.0 + specifier: ^10.0.1 version: 10.0.1(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/github': - specifier: ^10.0.0 + specifier: ^10.3.5 version: 10.3.5(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/npm': specifier: ^13.1.5 version: 13.1.5(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/release-notes-generator': - specifier: ^14.0.0 - version: 14.1.0(semantic-release@25.0.3(typescript@6.0.3)) + specifier: ^14.1.1 + version: 14.1.1(semantic-release@25.0.3(typescript@6.0.3)) husky: specifier: ~9.1.7 version: 9.1.7 @@ -101,8 +101,8 @@ importers: specifier: 'catalog:' version: 25.0.3(typescript@6.0.3) turbo: - specifier: ^2 - version: 2.9.6 + specifier: ^2.9.16 + version: 2.9.16 packages/desktop: dependencies: @@ -233,10 +233,10 @@ importers: version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.7(vitest@4.1.5) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: 'catalog:' - version: 26.1.0 + version: 29.1.1 openapi-typescript: specifier: ^7.8.0 version: 7.13.0(typescript@6.0.3) @@ -248,7 +248,7 @@ importers: version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.7)(jsdom@26.1.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.8(@types/node@25.6.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) packages: @@ -267,13 +267,29 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} @@ -358,6 +374,10 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -378,82 +398,86 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.4.13': - resolution: {integrity: sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==} + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.13': - resolution: {integrity: sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==} + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.13': - resolution: {integrity: sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==} + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.13': - resolution: {integrity: sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==} + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.13': - resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.13': - resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.13': - resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.13': - resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.13': - resolution: {integrity: sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==} + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@commitlint/cli@20.5.2': - resolution: {integrity: sha512-IXr5xd3IX8SEG936P8gcpozRplkDeDSwJlt8UvoY1winwIy2udTbQ/cOCgbaaxcjdDqVoS29VUcz/wkwnSozbA==} + '@commitlint/cli@20.5.3': + resolution: {integrity: sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@20.5.0': - resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} + '@commitlint/config-conventional@20.5.3': + resolution: {integrity: sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==} engines: {node: '>=v18'} '@commitlint/config-validator@20.5.0': resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} engines: {node: '>=v18'} - '@commitlint/ensure@20.5.0': - resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} + '@commitlint/ensure@20.5.3': + resolution: {integrity: sha512-4i4AgNvH62owG9MwSiWKrle7HGNpBHHdLnWFIp5fTsHUYe5kRuh15t08L/0pdbbrRk8JKXQxxN4hZQcn+szkrw==} engines: {node: '>=v18'} '@commitlint/execute-rule@20.0.0': @@ -468,12 +492,12 @@ packages: resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} engines: {node: '>=v18'} - '@commitlint/lint@20.5.0': - resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} + '@commitlint/lint@20.5.3': + resolution: {integrity: sha512-M7JbWBNr2gXKaPc4i/KipsuW1gkDHpj35KPjWtKy3Z+2AQw5wu1gBi1LIO0uoaij67CqY4K8PxPZSGens4evCw==} engines: {node: '>=v18'} - '@commitlint/load@20.5.2': - resolution: {integrity: sha512-zmr0RGDz7vThxW1I8ohb9yBjnGuH9mqwJpn21hInjGla+IlLOkS9ey0+dD5HlkzFlY0lX2NYdA2lDW6/0rO7Gw==} + '@commitlint/load@20.5.3': + resolution: {integrity: sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ==} engines: {node: '>=v18'} '@commitlint/message@20.4.3': @@ -488,12 +512,12 @@ packages: resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} engines: {node: '>=v18'} - '@commitlint/resolve-extends@20.5.2': - resolution: {integrity: sha512-8EhSCU9eNos/5cI1yg64GW79UH1c64O69AfStCsj4zqy6An/qIphVEXj4/+2M6056T8coz00f+UXFn4WUUP1HQ==} + '@commitlint/resolve-extends@20.5.3': + resolution: {integrity: sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew==} engines: {node: '>=v18'} - '@commitlint/rules@20.5.0': - resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} + '@commitlint/rules@20.5.3': + resolution: {integrity: sha512-MPlMnb9D3wbszYMp+1hPtuhtPJndRo6I6yfkZVA4+jR8w7Kqp0u2u/Y+gzbaItx5Lltq5rw7FSZQWJMoXUC4NQ==} engines: {node: '>=v18'} '@commitlint/to-lines@20.0.0': @@ -520,33 +544,41 @@ packages: conventional-commits-parser: optional: true - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} + '@csstools/css-color-parser@4.1.1': + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -713,6 +745,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -853,8 +894,8 @@ packages: resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} engines: {node: '>= 20'} - '@octokit/request@10.0.8': - resolution: {integrity: sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==} + '@octokit/request@10.0.10': + resolution: {integrity: sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==} engines: {node: '>= 20'} '@octokit/request@9.2.4': @@ -1347,8 +1388,8 @@ packages: peerDependencies: semantic-release: '>=20.1.0' - '@semantic-release/github@12.0.6': - resolution: {integrity: sha512-aYYFkwHW3c6YtHwQF0t0+lAjlU+87NFOZuH2CvWFD0Ylivc7MwhZMiHOJ0FMpIgPpCVib/VUAcOwvrW0KnxQtA==} + '@semantic-release/github@12.0.8': + resolution: {integrity: sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==} engines: {node: ^22.14.0 || >= 24.10.0} peerDependencies: semantic-release: '>=24.1.0' @@ -1359,8 +1400,8 @@ packages: peerDependencies: semantic-release: '>=20.1.0' - '@semantic-release/release-notes-generator@14.1.0': - resolution: {integrity: sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==} + '@semantic-release/release-notes-generator@14.1.1': + resolution: {integrity: sha512-Pbd2e2XRMUD0OxehHpgd5/YghsE76cddkRHSoDvKLK+OCy4Ewxn49rWR631MEUU01lgwF/uyVXvbnVuu6+Z6VA==} engines: {node: '>=20.8.1'} peerDependencies: semantic-release: '>=20.1.0' @@ -1670,33 +1711,33 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@turbo/darwin-64@2.9.6': - resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} + '@turbo/darwin-64@2.9.16': + resolution: {integrity: sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.6': - resolution: {integrity: sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw==} + '@turbo/darwin-arm64@2.9.16': + resolution: {integrity: sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.6': - resolution: {integrity: sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA==} + '@turbo/linux-64@2.9.16': + resolution: {integrity: sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.6': - resolution: {integrity: sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g==} + '@turbo/linux-arm64@2.9.16': + resolution: {integrity: sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.6': - resolution: {integrity: sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g==} + '@turbo/windows-64@2.9.16': + resolution: {integrity: sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.6': - resolution: {integrity: sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A==} + '@turbo/windows-arm64@2.9.16': + resolution: {integrity: sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==} cpu: [arm64] os: [win32] @@ -1769,20 +1810,20 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/coverage-v8@4.1.7': - resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} + '@vitest/coverage-v8@4.1.8': + resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} peerDependencies: - '@vitest/browser': 4.1.7 - vitest: 4.1.7 + '@vitest/browser': 4.1.8 + vitest: 4.1.8 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.5': - resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.1.5': - resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1792,26 +1833,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.5': - resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/runner@4.1.5': - resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} - '@vitest/snapshot@4.1.5': - resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} - '@vitest/spy@4.1.5': - resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - - '@vitest/utils@4.1.5': - resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} - - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -1822,6 +1857,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1923,6 +1962,9 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2051,6 +2093,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + conventional-changelog-angular@8.3.1: resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} engines: {node: '>=18'} @@ -2111,19 +2157,19 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -2195,9 +2241,9 @@ packages: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} env-ci@11.2.0: resolution: {integrity: sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA==} @@ -2217,6 +2263,9 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -2262,9 +2311,6 @@ packages: fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} - fast-content-type-parse@3.0.0: - resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2272,8 +2318,8 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2325,11 +2371,8 @@ packages: react-dom: optional: true - from2@2.3.0: - resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} - - fs-extra@11.3.4: - resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} engines: {node: '>=14.14'} fsevents@2.3.2: @@ -2354,8 +2397,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} get-nonce@1.0.1: @@ -2366,10 +2409,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@7.0.1: - resolution: {integrity: sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==} - engines: {node: '>=16'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -2437,13 +2476,13 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - hosted-git-info@9.0.2: - resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} engines: {node: ^20.17.0 || >=22.9.0} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2455,10 +2494,18 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-agent@9.0.0: + resolution: {integrity: sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==} + engines: {node: '>= 20'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https-proxy-agent@9.0.0: + resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} + engines: {node: '>= 20'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2476,10 +2523,6 @@ packages: engines: {node: '>=18'} hasBin: true - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -2520,10 +2563,6 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - into-stream@7.0.0: - resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==} - engines: {node: '>=12'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -2596,8 +2635,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - issue-parser@7.0.1: - resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} + issue-parser@7.0.2: + resolution: {integrity: sha512-7atWPjhGEIX3JEtMrOYd8TKzboYlq+5sNbdl9POiLYOI14G5HZiQbZP0Xj5EZdrufQVXfJlpTV0hys0CuxwxZw==} engines: {node: ^18.17 || >=20.6.1} istanbul-lib-coverage@3.2.2: @@ -2634,9 +2673,13 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -2756,9 +2799,6 @@ packages: lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.capitalize@4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} @@ -2771,24 +2811,9 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lodash.uniqby@4.7.0: resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2798,8 +2823,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.5: - resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -2891,6 +2916,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} @@ -3056,8 +3084,8 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-url@9.0.0: - resolution: {integrity: sha512-z9nC87iaZXXySbWWtTHfCFJyFvKaUAW6lODhikG7ILSbVgmwuFjUqkgnheHvAUcGedO29e2QGBRXMUD64aurqQ==} + normalize-url@9.0.1: + resolution: {integrity: sha512-ARftfC5HdUNu9jJeL8pHj8debUIHA2b91FizCoMzY4lG6dDX13jdvTK0TBe24IBDRf2HvJSzzwEPvmbkQWHRSg==} engines: {node: '>=20'} npm-run-path@4.0.1: @@ -3072,8 +3100,8 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - npm@11.13.0: - resolution: {integrity: sha512-cRmhaghDWA1lFgl3Ug4/VxDJdPBK/U+tNtnrl9kXunFqhWw1x4xL5txkNn7qzPuVfvXOmXyjHpMwsuk2uisbkg==} + npm@11.16.0: + resolution: {integrity: sha512-A74XL8OxmcegZDMWPkWb5bEQppg8HdYwW3rBD2sPoS4UQHVajfaxBkqyzLeJ3wR0kZ+5xoTjItxXaF7eIXUsyw==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true bundledDependencies: @@ -3143,9 +3171,6 @@ packages: - validate-npm-package-name - which - nwsapi@2.2.23: - resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3185,10 +3210,6 @@ packages: resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} engines: {node: '>=18'} - p-is-promise@3.0.0: - resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} - engines: {node: '>=8'} - p-limit@1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -3249,8 +3270,8 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} @@ -3464,18 +3485,12 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -3496,8 +3511,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -3693,36 +3708,40 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.1: - resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.4.2: + resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + tldts@7.4.2: + resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} hasBin: true to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} traverse@0.6.8: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} @@ -3746,8 +3765,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.6: - resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} + turbo@2.9.16: + resolution: {integrity: sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==} hasBin: true type-fest@1.4.0: @@ -3762,8 +3781,8 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type-fest@5.6.0: - resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + type-fest@5.7.0: + resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} typescript@6.0.3: @@ -3779,12 +3798,12 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - undici@6.25.0: - resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} + undici@6.26.0: + resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} engines: {node: '>=18.17'} - undici@7.25.0: - resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + undici@7.27.0: + resolution: {integrity: sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==} engines: {node: '>=20.18.1'} unicode-emoji-modifier-base@1.0.0: @@ -3929,20 +3948,20 @@ packages: yaml: optional: true - vitest@4.1.5: - resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.5 - '@vitest/browser-preview': 4.1.5 - '@vitest/browser-webdriverio': 4.1.5 - '@vitest/coverage-istanbul': 4.1.5 - '@vitest/coverage-v8': 4.1.5 - '@vitest/ui': 4.1.5 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3977,25 +3996,20 @@ packages: web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -4018,18 +4032,6 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -4102,19 +4104,31 @@ snapshots: '@actions/http-client@4.0.1': dependencies: tunnel: 0.0.6 - undici: 6.25.0 + undici: 6.26.0 '@actions/io@3.0.2': {} '@adobe/css-tools@4.4.4': {} - '@asamuzakjp/css-color@3.2.0': + '@asamuzakjp/css-color@5.1.11': dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} '@babel/code-frame@7.29.0': dependencies: @@ -4122,6 +4136,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} '@babel/core@7.29.0': @@ -4215,6 +4235,8 @@ snapshots: '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -4245,52 +4267,56 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.4.13': + '@biomejs/biome@2.4.16': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.13 - '@biomejs/cli-darwin-x64': 2.4.13 - '@biomejs/cli-linux-arm64': 2.4.13 - '@biomejs/cli-linux-arm64-musl': 2.4.13 - '@biomejs/cli-linux-x64': 2.4.13 - '@biomejs/cli-linux-x64-musl': 2.4.13 - '@biomejs/cli-win32-arm64': 2.4.13 - '@biomejs/cli-win32-x64': 2.4.13 + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 - '@biomejs/cli-darwin-arm64@2.4.13': + '@biomejs/cli-darwin-arm64@2.4.16': optional: true - '@biomejs/cli-darwin-x64@2.4.13': + '@biomejs/cli-darwin-x64@2.4.16': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.13': + '@biomejs/cli-linux-arm64-musl@2.4.16': optional: true - '@biomejs/cli-linux-arm64@2.4.13': + '@biomejs/cli-linux-arm64@2.4.16': optional: true - '@biomejs/cli-linux-x64-musl@2.4.13': + '@biomejs/cli-linux-x64-musl@2.4.16': optional: true - '@biomejs/cli-linux-x64@2.4.13': + '@biomejs/cli-linux-x64@2.4.16': optional: true - '@biomejs/cli-win32-arm64@2.4.13': + '@biomejs/cli-win32-arm64@2.4.16': optional: true - '@biomejs/cli-win32-x64@2.4.13': + '@biomejs/cli-win32-x64@2.4.16': optional: true + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@colors/colors@1.5.0': optional: true - '@commitlint/cli@20.5.2(@types/node@25.6.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)': + '@commitlint/cli@20.5.3(@types/node@25.6.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)': dependencies: '@commitlint/format': 20.5.0 - '@commitlint/lint': 20.5.0 - '@commitlint/load': 20.5.2(@types/node@25.6.0)(typescript@6.0.3) + '@commitlint/lint': 20.5.3 + '@commitlint/load': 20.5.3(@types/node@25.6.0)(typescript@6.0.3) '@commitlint/read': 20.5.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) '@commitlint/types': 20.5.0 - tinyexec: 1.1.1 + tinyexec: 1.2.4 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' @@ -4298,7 +4324,7 @@ snapshots: - conventional-commits-parser - typescript - '@commitlint/config-conventional@20.5.0': + '@commitlint/config-conventional@20.5.3': dependencies: '@commitlint/types': 20.5.0 conventional-changelog-conventionalcommits: 9.3.1 @@ -4308,14 +4334,10 @@ snapshots: '@commitlint/types': 20.5.0 ajv: 8.20.0 - '@commitlint/ensure@20.5.0': + '@commitlint/ensure@20.5.3': dependencies: '@commitlint/types': 20.5.0 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.startcase: 4.4.0 - lodash.upperfirst: 4.3.1 + es-toolkit: 1.47.0 '@commitlint/execute-rule@20.0.0': {} @@ -4327,25 +4349,25 @@ snapshots: '@commitlint/is-ignored@20.5.0': dependencies: '@commitlint/types': 20.5.0 - semver: 7.7.4 + semver: 7.8.1 - '@commitlint/lint@20.5.0': + '@commitlint/lint@20.5.3': dependencies: '@commitlint/is-ignored': 20.5.0 '@commitlint/parse': 20.5.0 - '@commitlint/rules': 20.5.0 + '@commitlint/rules': 20.5.3 '@commitlint/types': 20.5.0 - '@commitlint/load@20.5.2(@types/node@25.6.0)(typescript@6.0.3)': + '@commitlint/load@20.5.3(@types/node@25.6.0)(typescript@6.0.3)': dependencies: '@commitlint/config-validator': 20.5.0 '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.5.2 + '@commitlint/resolve-extends': 20.5.3 '@commitlint/types': 20.5.0 cosmiconfig: 9.0.1(typescript@6.0.3) cosmiconfig-typescript-loader: 6.3.0(@types/node@25.6.0)(cosmiconfig@9.0.1(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.47.0 is-plain-obj: 4.1.0 - lodash.mergewith: 4.6.2 picocolors: 1.1.1 transitivePeerDependencies: - '@types/node' @@ -4365,23 +4387,23 @@ snapshots: '@commitlint/types': 20.5.0 git-raw-commits: 5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) minimist: 1.2.8 - tinyexec: 1.1.1 + tinyexec: 1.2.4 transitivePeerDependencies: - conventional-commits-filter - conventional-commits-parser - '@commitlint/resolve-extends@20.5.2': + '@commitlint/resolve-extends@20.5.3': dependencies: '@commitlint/config-validator': 20.5.0 '@commitlint/types': 20.5.0 + es-toolkit: 1.47.0 global-directory: 5.0.0 import-meta-resolve: 4.2.0 - lodash.mergewith: 4.6.2 resolve-from: 5.0.0 - '@commitlint/rules@20.5.0': + '@commitlint/rules@20.5.3': dependencies: - '@commitlint/ensure': 20.5.0 + '@commitlint/ensure': 20.5.3 '@commitlint/message': 20.4.3 '@commitlint/to-lines': 20.0.0 '@commitlint/types': 20.5.0 @@ -4401,30 +4423,34 @@ snapshots: dependencies: '@simple-libs/child-process-utils': 1.0.2 '@simple-libs/stream-utils': 1.2.0 - semver: 7.7.4 + semver: 7.8.1 optionalDependencies: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 - '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} '@emnapi/core@1.10.0': dependencies: @@ -4520,6 +4546,8 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@exodus/bytes@1.15.1': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -4597,7 +4625,7 @@ snapshots: dependencies: '@octokit/auth-token': 6.0.0 '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.8 + '@octokit/request': 10.0.10 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 before-after-hook: 4.0.0 @@ -4621,7 +4649,7 @@ snapshots: '@octokit/graphql@9.0.3': dependencies: - '@octokit/request': 10.0.8 + '@octokit/request': 10.0.10 '@octokit/types': 16.0.0 universal-user-agent: 7.0.3 @@ -4675,12 +4703,12 @@ snapshots: dependencies: '@octokit/types': 16.0.0 - '@octokit/request@10.0.8': + '@octokit/request@10.0.10': dependencies: '@octokit/endpoint': 11.0.3 '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 - fast-content-type-parse: 3.0.0 + content-type: 2.0.0 json-with-bigint: 3.5.8 universal-user-agent: 7.0.3 @@ -5069,7 +5097,7 @@ snapshots: dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 - fs-extra: 11.3.4 + fs-extra: 11.3.5 lodash: 4.18.1 semantic-release: 25.0.3(typescript@6.0.3) @@ -5130,7 +5158,7 @@ snapshots: globby: 14.1.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6(supports-color@10.2.2) - issue-parser: 7.0.1 + issue-parser: 7.0.2 lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 @@ -5139,7 +5167,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@semantic-release/github@12.0.6(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/github@12.0.8(semantic-release@25.0.3(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) @@ -5149,15 +5177,15 @@ snapshots: aggregate-error: 5.0.0 debug: 4.4.3(supports-color@10.2.2) dir-glob: 3.0.1 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6(supports-color@10.2.2) - issue-parser: 7.0.1 + http-proxy-agent: 9.0.0 + https-proxy-agent: 9.0.0 + issue-parser: 7.0.2 lodash-es: 4.18.1 mime: 4.1.0 p-filter: 4.1.0 semantic-release: 25.0.3(typescript@6.0.3) - tinyglobby: 0.2.16 - undici: 7.25.0 + tinyglobby: 0.2.17 + undici: 7.27.0 url-join: 5.0.0 transitivePeerDependencies: - supports-color @@ -5169,28 +5197,26 @@ snapshots: aggregate-error: 5.0.0 env-ci: 11.2.0 execa: 9.6.1 - fs-extra: 11.3.4 + fs-extra: 11.3.5 lodash-es: 4.18.1 nerf-dart: 1.0.0 - normalize-url: 9.0.0 - npm: 11.13.0 + normalize-url: 9.0.1 + npm: 11.16.0 rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.1 semantic-release: 25.0.3(typescript@6.0.3) - semver: 7.7.4 + semver: 7.8.1 tempy: 3.2.0 - '@semantic-release/release-notes-generator@14.1.0(semantic-release@25.0.3(typescript@6.0.3))': + '@semantic-release/release-notes-generator@14.1.1(semantic-release@25.0.3(typescript@6.0.3))': dependencies: conventional-changelog-angular: 8.3.1 conventional-changelog-writer: 8.4.0 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 debug: 4.4.3(supports-color@10.2.2) - get-stream: 7.0.1 import-from-esm: 2.0.0 - into-stream: 7.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 semantic-release: 25.0.3(typescript@6.0.3) @@ -5428,8 +5454,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -5460,22 +5486,22 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@turbo/darwin-64@2.9.6': + '@turbo/darwin-64@2.9.16': optional: true - '@turbo/darwin-arm64@2.9.6': + '@turbo/darwin-arm64@2.9.16': optional: true - '@turbo/linux-64@2.9.6': + '@turbo/linux-64@2.9.16': optional: true - '@turbo/linux-arm64@2.9.6': + '@turbo/linux-arm64@2.9.16': optional: true - '@turbo/windows-64@2.9.6': + '@turbo/windows-64@2.9.16': optional: true - '@turbo/windows-arm64@2.9.6': + '@turbo/windows-arm64@2.9.16': optional: true '@tybys/wasm-util@0.10.1': @@ -5539,10 +5565,10 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) - '@vitest/coverage-v8@4.1.7(vitest@4.1.5)': + '@vitest/coverage-v8@4.1.8(vitest@4.1.8)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.8 ast-v8-to-istanbul: 1.0.2 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -5551,56 +5577,46 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.7)(jsdom@26.1.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.8(@types/node@25.6.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) - '@vitest/expect@4.1.5': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.8(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))': dependencies: - '@vitest/spy': 4.1.5 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) - '@vitest/pretty-format@4.1.5': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - '@vitest/pretty-format@4.1.7': + '@vitest/runner@4.1.8': dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.5': - dependencies: - '@vitest/utils': 4.1.5 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.1.5': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.5': {} + '@vitest/spy@4.1.8': {} - '@vitest/utils@4.1.5': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.5 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - - '@vitest/utils@4.1.7': - dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -5608,6 +5624,8 @@ snapshots: agent-base@7.1.4: {} + agent-base@9.0.0: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -5621,7 +5639,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5699,6 +5717,10 @@ snapshots: before-after-hook@4.0.0: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bottleneck@2.19.5: {} @@ -5835,6 +5857,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + content-type@2.0.0: {} + conventional-changelog-angular@8.3.1: dependencies: compare-func: 2.0.0 @@ -5849,7 +5873,7 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.9 meow: 13.2.0 - semver: 7.7.4 + semver: 7.8.1 conventional-commits-filter@5.0.0: {} @@ -5877,7 +5901,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -5892,19 +5916,21 @@ snapshots: dependencies: type-fest: 1.4.0 - css.escape@1.5.1: {} - - cssstyle@4.6.0: + css-tree@3.2.1: dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} csstype@3.2.3: {} - data-urls@5.0.0: + data-urls@7.0.0: dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' debug@4.4.3(supports-color@10.2.2): dependencies: @@ -5961,7 +5987,7 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - entities@6.0.1: {} + entities@8.0.0: {} env-ci@11.2.0: dependencies: @@ -5978,6 +6004,8 @@ snapshots: es-module-lexer@2.1.0: {} + es-toolkit@1.47.0: {} + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -6064,8 +6092,6 @@ snapshots: fast-content-type-parse@2.0.1: {} - fast-content-type-parse@3.0.0: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6076,7 +6102,7 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fastq@1.20.1: dependencies: @@ -6118,12 +6144,7 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - from2@2.3.0: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - - fs-extra@11.3.4: + fs-extra@11.3.5: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 @@ -6141,14 +6162,12 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} + get-east-asian-width@1.6.0: {} get-nonce@1.0.1: {} get-stream@6.0.1: {} - get-stream@7.0.1: {} - get-stream@8.0.1: {} get-stream@9.0.1: @@ -6243,13 +6262,15 @@ snapshots: dependencies: lru-cache: 10.4.3 - hosted-git-info@9.0.2: + hosted-git-info@9.0.3: dependencies: - lru-cache: 11.3.5 + lru-cache: 11.5.1 - html-encoding-sniffer@4.0.0: + html-encoding-sniffer@6.0.0: dependencies: - whatwg-encoding: 3.1.1 + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' html-escaper@2.0.2: {} @@ -6262,6 +6283,13 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 @@ -6269,6 +6297,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -6277,10 +6312,6 @@ snapshots: husky@9.1.7: {} - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - ignore@7.0.5: {} import-fresh@3.3.1: @@ -6311,11 +6342,6 @@ snapshots: inline-style-parser@0.2.7: {} - into-stream@7.0.0: - dependencies: - from2: 2.3.0 - p-is-promise: 3.0.0 - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -6363,7 +6389,7 @@ snapshots: isexe@2.0.0: {} - issue-parser@7.0.1: + issue-parser@7.0.2: dependencies: lodash.capitalize: 4.2.1 lodash.escaperegexp: 4.1.2 @@ -6398,32 +6424,35 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: + js-yaml@4.2.0: dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 + argparse: 2.0.1 + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6(supports-color@10.2.2) + html-encoding-sniffer: 6.0.0 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.23 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 + lru-cache: 11.5.1 + parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 6.0.1 + undici: 7.27.0 w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.20.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + - '@noble/hashes' jsesc@3.1.0: {} @@ -6508,8 +6537,6 @@ snapshots: lodash-es@4.18.1: {} - lodash.camelcase@4.3.0: {} - lodash.capitalize@4.2.1: {} lodash.escaperegexp@4.1.2: {} @@ -6518,25 +6545,15 @@ snapshots: lodash.isstring@4.0.1: {} - lodash.kebabcase@4.1.1: {} - - lodash.mergewith@4.6.2: {} - - lodash.snakecase@4.1.1: {} - - lodash.startcase@4.4.0: {} - lodash.uniqby@4.7.0: {} - lodash.upperfirst@4.3.1: {} - lodash@4.18.1: {} longest-streak@3.1.0: {} lru-cache@10.4.3: {} - lru-cache@11.3.5: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: dependencies: @@ -6557,7 +6574,7 @@ snapshots: magicast@0.5.3: dependencies: '@babel/parser': 7.29.7 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-asynchronous@1.1.0: @@ -6568,7 +6585,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.1 markdown-table@3.0.4: {} @@ -6738,6 +6755,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} + meow@13.2.0: {} merge-stream@2.0.0: {} @@ -6986,18 +7005,18 @@ snapshots: normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.4 + semver: 7.8.1 validate-npm-package-license: 3.0.4 normalize-package-data@8.0.0: dependencies: - hosted-git-info: 9.0.2 - semver: 7.7.4 + hosted-git-info: 9.0.3 + semver: 7.8.1 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} - normalize-url@9.0.0: {} + normalize-url@9.0.1: {} npm-run-path@4.0.1: dependencies: @@ -7012,9 +7031,7 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - npm@11.13.0: {} - - nwsapi@2.2.23: {} + npm@11.16.0: {} object-assign@4.1.1: {} @@ -7054,8 +7071,6 @@ snapshots: dependencies: p-map: 7.0.4 - p-is-promise@3.0.0: {} - p-limit@1.3.0: dependencies: p-try: 1.0.0 @@ -7095,7 +7110,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -7116,9 +7131,9 @@ snapshots: parse5@6.0.1: {} - parse5@7.3.0: + parse5@8.0.1: dependencies: - entities: 6.0.1 + entities: 8.0.0 path-exists@3.0.0: {} @@ -7254,14 +7269,14 @@ snapshots: dependencies: find-up-simple: 1.0.1 read-pkg: 10.1.0 - type-fest: 5.6.0 + type-fest: 5.7.0 read-pkg@10.1.0: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 8.0.0 parse-json: 8.3.0 - type-fest: 5.6.0 + type-fest: 5.7.0 unicorn-magic: 0.4.0 read-pkg@9.0.1: @@ -7362,16 +7377,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rrweb-cssom@0.8.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safe-buffer@5.1.2: {} - safer-buffer@2.1.2: {} - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -7382,9 +7393,9 @@ snapshots: dependencies: '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 12.0.6(semantic-release@25.0.3(typescript@6.0.3)) + '@semantic-release/github': 12.0.8(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/npm': 13.1.5(semantic-release@25.0.3(typescript@6.0.3)) - '@semantic-release/release-notes-generator': 14.1.0(semantic-release@25.0.3(typescript@6.0.3)) + '@semantic-release/release-notes-generator': 14.1.1(semantic-release@25.0.3(typescript@6.0.3)) aggregate-error: 5.0.0 cosmiconfig: 9.0.1(typescript@6.0.3) debug: 4.4.3(supports-color@10.2.2) @@ -7395,7 +7406,7 @@ snapshots: get-stream: 6.0.1 git-log-parser: 1.2.1 hook-std: 4.0.0 - hosted-git-info: 9.0.2 + hosted-git-info: 9.0.3 import-from-esm: 2.0.0 lodash-es: 4.18.1 marked: 15.0.12 @@ -7405,7 +7416,7 @@ snapshots: p-reduce: 3.0.0 read-package-up: 12.0.0 resolve-from: 5.0.0 - semver: 7.7.4 + semver: 7.8.1 signale: 1.4.0 yargs: 18.0.0 transitivePeerDependencies: @@ -7416,7 +7427,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.8.1: {} seroval-plugins@1.5.2(seroval@1.5.2): dependencies: @@ -7492,7 +7503,7 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string_decoder@1.1.1: @@ -7593,30 +7604,35 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.1.1: {} + tinyexec@1.2.4: {} tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.1.0: {} - tldts-core@6.1.86: {} + tldts-core@7.4.2: {} - tldts@6.1.86: + tldts@7.4.2: dependencies: - tldts-core: 6.1.86 + tldts-core: 7.4.2 to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - tough-cookie@5.1.2: + tough-cookie@6.0.1: dependencies: - tldts: 6.1.86 + tldts: 7.4.2 - tr46@5.1.1: + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -7637,14 +7653,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.9.6: + turbo@2.9.16: optionalDependencies: - '@turbo/darwin-64': 2.9.6 - '@turbo/darwin-arm64': 2.9.6 - '@turbo/linux-64': 2.9.6 - '@turbo/linux-arm64': 2.9.6 - '@turbo/windows-64': 2.9.6 - '@turbo/windows-arm64': 2.9.6 + '@turbo/darwin-64': 2.9.16 + '@turbo/darwin-arm64': 2.9.16 + '@turbo/linux-64': 2.9.16 + '@turbo/linux-arm64': 2.9.16 + '@turbo/windows-64': 2.9.16 + '@turbo/windows-arm64': 2.9.16 type-fest@1.4.0: {} @@ -7652,7 +7668,7 @@ snapshots: type-fest@4.41.0: {} - type-fest@5.6.0: + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 @@ -7663,9 +7679,9 @@ snapshots: undici-types@7.19.2: {} - undici@6.25.0: {} + undici@6.26.0: {} - undici@7.25.0: {} + undici@7.27.0: {} unicode-emoji-modifier-base@1.0.0: {} @@ -7783,15 +7799,15 @@ snapshots: jiti: 2.6.1 tsx: 4.21.0 - vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.7)(jsdom@26.1.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.8(@types/node@25.6.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)): dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -7800,15 +7816,15 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/coverage-v8': 4.1.7(vitest@4.1.5) - jsdom: 26.1.0 + '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) + jsdom: 29.1.1 transitivePeerDependencies: - msw @@ -7818,20 +7834,19 @@ snapshots: web-worker@1.5.0: {} - webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} webpack-virtual-modules@0.6.2: {} - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 + whatwg-mimetype@5.0.0: {} - whatwg-mimetype@4.0.0: {} - - whatwg-url@14.2.0: + whatwg-url@16.0.1: dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' which@2.0.2: dependencies: @@ -7856,8 +7871,6 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - ws@8.20.0: {} - xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f3f6482..e3822fd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,7 @@ allowBuilds: catalog: # Standard tooling - "@biomejs/biome": "^2.4.0" + "@biomejs/biome": "^2.4.16" # Testing "@playwright/test": "^1.59.1" @@ -15,16 +15,16 @@ catalog: "@types/react": "^19.2.14" "@types/react-dom": "^19.2.3" "@vitejs/plugin-react": "^6.0.1" - "@vitest/coverage-v8": "^4.0.0" - jsdom: "^26.1.0" + "@vitest/coverage-v8": "^4.1.8" + jsdom: "^29.1.1" # React react: "^19.2.5" react-dom: "^19.2.5" - semantic-release: "^25.0.0" + semantic-release: "^25.0.3" typescript: "~6.0.2" # Vite vite: "^8.0.10" - vitest: "^4.0.0" + vitest: "^4.1.8" zod: "^4.0.0" diff --git a/scripts/secret-scan.sh b/scripts/secret-scan.sh index 69f4517..f74265a 100755 --- a/scripts/secret-scan.sh +++ b/scripts/secret-scan.sh @@ -52,12 +52,17 @@ check_pattern "Honcho-style JWT (likely)" 'eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_ check_pattern "RSA/EC/DSA/OpenSSH private key block" '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----' check_pattern "Generic hardcoded password" '(password|passwd|pwd)[[:space:]]*[:=][[:space:]]*["'\'']\w{8,}["'\'']' +# Environment-specific values — keep live infra out of committed code/docs/PRs. +# Use examples instead (honcho.example.net; 192.0.2.x per RFC 5737 TEST-NET). +check_pattern "Tailnet hostname (env-specific; use example.net)" '[A-Za-z0-9-]+\.ts\.net' +check_pattern "Tailnet/CGNAT IP (env-specific; use 192.0.2.x)" '100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]{1,3}\.[0-9]{1,3}' + if [ $FOUND -eq 1 ]; then - printf '\n\033[31m✗ Secret scan: potential secrets in staged changes\033[0m\n' >&2 + printf '\n\033[31m✗ Secret scan: potential secrets or environment-specific values in staged changes\033[0m\n' >&2 printf '%b' "$FINDINGS" >&2 printf '\n' >&2 printf 'If this is a false positive, bypass with: \033[33mgit commit --no-verify\033[0m\n' >&2 - printf 'Otherwise: remove the secret, rotate the credential, and re-stage.\n\n' >&2 + printf 'Otherwise: remove the secret/value (use an example), rotate if a credential, and re-stage.\n\n' >&2 exit 1 fi