From 04ff2138d2dc18ec7d572bace03c84337e9abc85 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Sat, 16 May 2026 20:15:04 -0400 Subject: [PATCH] feat: initial fate based SPA --- .gitignore | 1 + CODEBASE_FINDINGS.md | 598 ++++++++++++++++++++++++++++++++++++++++ app/server/hono-app.ts | 107 +++++++ app/server/hono-dev.ts | 87 ++++++ app/server/hono-main.ts | 46 ++++ app/spa/app.tsx | 21 ++ app/spa/main.tsx | 16 ++ app/spa/router.tsx | 69 +++++ index.html | 12 + package.json | 13 +- pnpm-lock.yaml | 504 +++++++++++++++++++++++++++++++-- vite-old.config.ts | 90 ++++++ vite.config.ts | 55 ++-- 13 files changed, 1549 insertions(+), 70 deletions(-) create mode 100644 CODEBASE_FINDINGS.md create mode 100644 app/server/hono-app.ts create mode 100644 app/server/hono-dev.ts create mode 100644 app/server/hono-main.ts create mode 100644 app/spa/app.tsx create mode 100644 app/spa/main.tsx create mode 100644 app/spa/router.tsx create mode 100644 index.html create mode 100644 vite-old.config.ts diff --git a/.gitignore b/.gitignore index 92458f7..eaba650 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules /.react-router /.cache +/.data /build /test .env diff --git a/CODEBASE_FINDINGS.md b/CODEBASE_FINDINGS.md new file mode 100644 index 0000000..68f59e9 --- /dev/null +++ b/CODEBASE_FINDINGS.md @@ -0,0 +1,598 @@ +# Headplane codebase findings + +Date: 2026-05-16 + +Scope: server-side TypeScript, React Router route modules, client components, live-data/data-fetching paths, auth/session/RBAC, Headscale config/API integration, Go agent/WebSSH code, runtime/build/CI, and docs. This is an architecture and pattern review, not a completed security audit. + +## Executive summary + +Headplane's biggest codebase risks are not isolated style issues; they cluster around ownership boundaries: + +- **Auth/session boundary:** API-key sessions put the raw Headscale API key in the auth cookie, and self-service account linking can claim arbitrary unclaimed Headscale users. +- **Live-data boundary:** the current live path polls globally, caches globally, and tells React Router to re-run whole active route loaders on any resource change. +- **Lifecycle boundary:** process-lifetime services have `setInterval`, child processes, SSE streams, Undici agents, and WASM/tsnet state, but there is no coordinated shutdown/disposal model. +- **Mutation boundary:** config-file edits and several admin/user mutations rely on UI constraints, mutable process state, non-transactional writes, or inconsistent server-side validation. +- **Framework/tooling boundary:** React Router loaders/actions currently carry a lot of app state orchestration. Fate could be a good fit for live `nodes`/`users` data, but it will not fix auth, authorization, config mutation, WebSSH, or agent lifecycle issues by itself. + +## Highest-priority findings + +### 1. OIDC self-linking can claim arbitrary unclaimed Headscale users + +**Severity:** Critical +**Area:** AuthZ / account linking + +**Evidence:** + +- `app/routes/home.tsx:84-97` accepts a posted `headscale_user_id` and calls `context.auth.linkHeadscaleUser(principal.user.id, headscaleUserId)`. +- `app/server/web/auth.ts:225-237` grants machine ownership based on `principal.user.headscaleUserId === node.user?.id`. +- `app/routes/machines/machine-actions.ts:57-68` authorizes machine mutations through `canManageNode`. + +**Why it matters:** a logged-in OIDC user can forge the form POST and link themselves to any unclaimed Headscale user ID. Once linked, they can manage that user's machines through the machine action authorization path. + +**Suggested direction:** recompute allowed self-link targets server-side inside the action and reject anything not in that set. Prefer auto-linking only by verified OIDC subject/email match, and reserve arbitrary linking for admins. + +### 2. API-key sessions put the raw Headscale API key in a signed cookie + +**Severity:** Critical +**Area:** Auth/session security + +**Evidence:** + +- `app/server/web/auth.ts:96-105` manually base64url-encodes JSON and signs it with HMAC. +- `app/server/web/auth.ts:269-283` serializes `{ sid, api_key }` for API-key sessions. +- `app/server/web/auth.ts:144-173` resolves API-key principals from `payload.api_key`; the DB `api_key_hash` is not used to retrieve a server-side secret. +- `app/utils/oidc-state.ts:13-19` explicitly sets `httpOnly`, while the main auth cookie options are not explicit in `app/server/context.ts:49-54` / `app/server/web/auth.ts:96-105`. +- `docs/install/index.md` describes `server.cookie_secret` as encrypting cookies, but the implementation signs rather than encrypts. + +**Why it matters:** cookie compromise becomes direct Headscale API compromise, not just Headplane session compromise. HMAC protects integrity, not confidentiality. The auth cookie should be an opaque session handle, not a transport for upstream credentials. + +**Suggested direction:** store only `sid` in the cookie; keep the API key encrypted or server-side in the DB/secret store; verify `api_key_hash` if retaining API-key sessions; explicitly set `httpOnly`, `sameSite`, `secure`, `path`, and consider key rotation/versioning. Use timing-safe HMAC comparison if manual signatures remain. + +## Live data and data-fetching findings + +### 3. SSE events trigger whole-route revalidation instead of resource-scoped updates + +**Severity:** High +**Area:** Client data fetching / live data + +**Evidence:** + +- `app/utils/live-data.tsx:35-39` calls `revalidator.revalidate()`. +- `app/utils/live-data.tsx:63-82` treats every `changed` event as a global route revalidation. +- `app/routes/util/live.ts:34-37` sends only `{ resource, version }`, not field/object-level deltas or subscribed views. + +**Why it matters:** any node/user change can re-run every active loader for the current route tree. Pages such as machines/users load multiple resources and agent metadata, so frequent Headscale changes can become over-fetching and UI churn. + +**Suggested direction:** short term, include enough resource metadata to revalidate only affected routes or debounce/coalesce revalidation. Long term, move live data to normalized object/list subscriptions instead of request/route invalidation. + +### 4. Live-data pause state is global, boolean, and can stick disabled + +**Severity:** High +**Area:** Client state / live data + +**Evidence:** + +- `app/root.tsx:41-66` mounts `LiveDataProvider` above all routes. +- `app/routes/auth/login/page.tsx:59-66` calls `pause()` with no dependency array and no cleanup/resume. +- `app/components/dialog.tsx:21-27` uses the same single boolean pause model for dialogs. +- `app/utils/live-data.tsx:136-141` exposes imperative `pause()` / `resume()` with no ownership token. + +**Why it matters:** visiting `/login` can leave live updates disabled after navigating into the app. Multiple pause consumers can also fight: closing one dialog can resume live updates while another still expects them paused. + +**Suggested direction:** add immediate cleanup in login (`pause(); return resume`) and a dependency array. Replace the boolean with a refcount/token model so each consumer releases only its own pause. + +### 5. `hsLive` is process-global but stores one mutable API client + +**Severity:** High +**Area:** Server live data / cache scoping + +**Evidence:** + +- `app/server/context.ts:86-101` creates one `hsLive` for the entire process. +- `app/server/headscale/live-store.ts:69` keeps `storedApiClient` in shared closure state. +- `app/server/headscale/live-store.ts:113-120` polling uses that shared client. +- `app/server/headscale/live-store.ts:143` and `app/server/headscale/live-store.ts:158` overwrite it from each caller. + +**Why it matters:** the last request/action to touch `hsLive` controls which API key future background polls use. A bad/expired user-supplied API-key session can poison live refresh for everyone until another request resets it. Snapshots are also not principal/session scoped. + +**Suggested direction:** poll with one stable server credential only, or scope cache/polling by principal/session. If adopting Fate, make this one of the first seams to replace. + +### 6. Live store polling is coarse and lifecycle-unmanaged + +**Severity:** Medium +**Area:** Server lifecycle / live data + +**Evidence:** + +- `app/server/headscale/live-store.ts:38-45` polls nodes every 5s and users every 15s. +- `app/server/headscale/live-store.ts:82-104` compares `JSON.stringify(data)` for the whole resource. +- `app/server/headscale/live-store.ts:113-123` starts intervals lazily but never stops them unless `dispose()` is called. +- `app/server/context.ts:94` creates the store, but no app shutdown path calls `hsLive.dispose()`. + +**Why it matters:** whole-resource JSON comparison is order-sensitive and grows with tailnet size. The polling intervals continue for process lifetime and can leak during dev HMR or repeated context creation. + +**Suggested direction:** introduce app-level lifecycle management and subscription-aware polling, or replace with event/object-scoped live subscriptions. + +### 7. Loader data is mirrored into local state and can overwrite edits during revalidation + +**Severity:** Medium +**Area:** Client state / React patterns + +**Evidence:** + +- `app/routes/acls/overview.tsx:30-41` mirrors `policy` into `codePolicy` and updates it when loader data changes. +- `app/routes/dns/components/manage-domains.tsx:25-31` mirrors `searchDomains` into local state. + +**Why it matters:** global SSE revalidation can replace local edit state. The ACL editor is especially risky because an unsaved policy can be overwritten by background loader refresh. + +**Suggested direction:** separate “initial server value” from “dirty draft” state; never overwrite dirty drafts on background revalidation without prompting. + +### 8. Fetcher/dialog orchestration is imperative and duplicated + +**Severity:** Medium +**Area:** Client forms / mutations + +**Evidence:** + +- `app/routes/machines/dialogs/tags.tsx:34-43` uses `submittingRef` + effects to close/unlock. +- `app/routes/settings/auth-keys/dialogs/add-auth-key.tsx:56-71` uses similar orchestration and directly assigns `fetcher.data = undefined`. +- `app/routes/machines/dialogs/routes.tsx:49-63` submits on switch changes without optimistic/error state. + +**Why it matters:** each dialog reimplements mutation lifecycle handling. Direct mutation of `fetcher.data` fights router-owned state and can hide stale or failed submissions. + +**Suggested direction:** centralize fetcher mutation state handling, or move to an action/mutation primitive with explicit pending/error/success states and optimistic updates. + +## Auth, authorization, and route handling findings + +### 9. Agent settings route/action lacks capability checks + +**Severity:** High +**Area:** Authorization + +**Evidence:** + +- `app/routes/settings/agent.tsx:13-27` only authenticates. +- `app/routes/settings/agent.tsx:29-39` only authenticates before triggering sync. + +**Why it matters:** any authenticated user can view agent status and trigger sync work if they know the URL, regardless of settings/admin capability. + +**Suggested direction:** gate read and sync separately, probably using `read_feature`/`write_feature` or a dedicated agent capability. + +### 10. App layout catches all loader errors and destroys sessions + +**Severity:** High +**Area:** Error handling / auth UX + +**Evidence:** + +- `app/layout/app.tsx:32-103` wraps the whole loader in `try/catch` and redirects to `/login` with `destroySession()` for any error. +- `app/layout/app.tsx:50-67` handles one expected expired-API-key case, but the outer catch covers unrelated failures too. + +**Why it matters:** network errors, programming errors, and transient Headscale failures can become silent logouts. It also hides diagnostics from route error boundaries. + +**Suggested direction:** catch only expected auth/session errors; let operational/programming errors hit the error boundary or a health banner. + +### 11. RBAC is inconsistent across routes/actions + +**Severity:** Medium/High +**Area:** Authorization + +**Evidence:** + +- `app/routes/machines/machine-actions.ts:64-68` performs resource-specific checks via `canManageNode`. +- `app/routes/users/user-actions.ts:10-17` checks only broad `write_users` before operations. +- `app/routes/home.tsx:84-97` lets OIDC users link Headscale identities without revalidating target ownership. +- `app/routes/settings/auth-keys/actions.ts:23-35` has more careful self-service ownership checks, showing the pattern exists but is not universal. + +**Why it matters:** permissions are enforced route-by-route with no single policy layer, so new actions can accidentally rely on UI hiding. The self-linking issue is one concrete outcome. + +**Suggested direction:** create server-side action guards for common ownership/role decisions and require every mutation to call one. + +### 12. Runtime role input is not validated and ownership updates are non-transactional + +**Severity:** Medium +**Area:** Auth/RBAC data integrity + +**Evidence:** + +- `app/routes/users/user-actions.ts:82-104` casts `newRole as Role` from form data. +- `app/server/web/auth.ts:467-484` upserts that role into the DB. +- `app/server/web/auth.ts:430-464` transfers ownership with two separate updates. +- `app/server/web/auth.ts:324-342` makes the first user owner with a count-after-insert flow. + +**Why it matters:** forged role values can reach storage, and failures/races in ownership transfer or first-login owner selection can produce invalid owner state. + +**Suggested direction:** validate `newRole` against `Roles` at the route boundary, use transactions for ownership transfer, and make first-owner assignment atomic. + +### 13. Login page has live-data and URL cleanup bugs + +**Severity:** Medium +**Area:** Client route behavior + +**Evidence:** + +- `app/routes/auth/login/page.tsx:59-66` pauses live data on every render and never resumes. +- `app/routes/auth/login/page.tsx:68-84` uses `window.history.replaceState` manually. +- `app/routes/auth/login/page.tsx:78-80` builds `newUrl` with `` `{${window.location.pathname}?...` ``, leaving a literal `{` in the URL. + +**Why it matters:** this can disable live updates and corrupt/uglify login URLs. Manual history mutation bypasses router state. + +**Suggested direction:** use a loader redirect or router navigation where possible; otherwise fix the string and make the effect one-shot with cleanup. + +## Headscale API and config mutation findings + +### 14. Headscale API errors are collapsed into `502 Bad Gateway` + +**Severity:** Medium/High +**Area:** API wrapper / error handling + +**Evidence:** + +- `app/server/headscale/api/index.ts:139-145` converts network errors to React Router `data(..., 502)`. +- `app/server/headscale/api/index.ts:183-203` converts every Headscale `>=400` response to outer status 502 while preserving the real status only inside the body. +- Many callers inspect raw strings/statuses manually, e.g. `app/routes/auth/login/action.ts:78-93` and `app/routes/acls/acl-action.ts:39-107`. + +**Why it matters:** UI and route logic must understand wrapper internals. HTTP semantics are obscured, and handling becomes string-fragile. + +**Suggested direction:** preserve upstream status classes where safe, expose typed domain errors, and make expected Headscale quirks explicit in one adapter layer. + +### 15. OpenAPI polling interval has no disposal and returned interface exposes stale values + +**Severity:** Medium +**Area:** Server lifecycle / API versioning + +**Evidence:** + +- `app/server/headscale/api/index.ts:241-250` starts a `setInterval` inside `createHeadscaleInterface`. +- `app/server/headscale/api/index.ts:252-272` returns `openapiHashes` and `apiVersion` as values captured at return time, while `clientHelpers.isAtleast` reads the mutable closure. +- No `dispose()` exists on the interface. + +**Why it matters:** lifecycle leaks in dev/reload scenarios, and consumers reading `context.hsApi.apiVersion` can see stale data while helpers see updated data. + +**Suggested direction:** add lifecycle, expose getters for mutable version state, or only detect once at startup. + +### 16. Config patching is not safely serialized + +**Severity:** Medium/High +**Area:** Config mutation / concurrency + +**Evidence:** + +- `app/server/headscale/config-loader.ts:63-127` mutates the YAML document before acquiring `writeLock`. +- `app/server/headscale/config-loader.ts:120-127` sets `writeLock = true` without `try/finally`; a failed write can leave the lock stuck. +- `app/server/headscale/config-dns.ts` has a similar write-lock pattern (see `writeLock` usage). + +**Why it matters:** concurrent admin actions can interleave mutations, lose updates, or deadlock future writes after an exception. + +**Suggested direction:** replace the boolean lock/spin loop with a promise queue or mutex; acquire before document mutation; release in `finally`; write atomically via temp file + rename. + +### 17. DNS/config actions mutate shared config arrays and lack input validation + +**Severity:** Medium +**Area:** Config mutation / validation + +**Evidence:** + +- `app/routes/dns/dns-actions.ts:103-118` pushes nameservers into arrays read from `context.hs.c`. +- `app/routes/dns/dns-actions.ts:147-164` pushes search domains into arrays read from `context.hs.c`. +- `app/routes/dns/dns-actions.ts:189-209` writes DNS record type/value from form data with minimal validation. + +**Why it matters:** in-memory config can be mutated before persistence succeeds, and invalid values can be written directly to Headscale config. + +**Suggested direction:** clone before modification, validate domain/IP/record types server-side, and return actionable typed errors. + +### 18. Restrictions config actions fire integration restarts without awaiting them + +**Severity:** Medium +**Area:** Config mutation / operational consistency + +**Evidence:** + +- `app/routes/settings/restrictions/actions.ts:51`, `:79`, `:100`, `:129`, `:150`, `:179` call `context.integration?.onConfigChange(api)` without `await`. +- `app/routes/dns/dns-actions.ts` generally awaits the same hook. + +**Why it matters:** the response can report success while restart/reload fails in the background, and unhandled rejections can be lost. + +**Suggested direction:** await the hook consistently or queue/retry restarts through an explicit background job with surfaced status. + +## Server lifecycle and runtime findings + +### 19. Process-lifetime services have no composition-root `stop()` + +**Severity:** High +**Area:** Server lifecycle + +**Evidence:** + +- `app/server/context.ts:21-101` constructs DB, Headscale API, live store, auth service, OIDC, optional agent manager, and integration. +- `app/server/app.ts:38-40` starts auth pruning. +- `app/server/hp-agent.ts:319-359` starts an interval and child process with `dispose()`. +- `app/server/headscale/live-store.ts:182-191` has `dispose()`. +- `runtime/http.ts:192-200` handles SIGINT/SIGTERM by closing the HTTP server and exiting, but has no app context cleanup. +- `runtime/vite-plugin.ts:44-50` uses `ssrLoadModule` in dev; HMR can recreate modules/services without calling old disposers. + +**Why it matters:** intervals, child processes, SSE listeners, and Undici agents can leak in dev and are force-killed in production instead of being drained. + +**Suggested direction:** return an app runtime with `stop()` from the composition root; call `auth.stop()`, `hsLive.dispose()`, `agents.dispose()`, `hsApi.undiciAgent.close()`, and integration cleanup from production shutdown and Vite HMR dispose hooks. + +### 20. Architecture docs describe patterns the code no longer fully follows + +**Severity:** Medium +**Area:** Documentation / maintainability + +**Evidence:** + +- `docs/development/architecture.md:15-20` says all services are closure factories, no classes/globals. +- `app/server/headscale/config-loader.ts:21-213` uses a mutable class for Headscale config. +- `docs/development/architecture.md:162-218` describes `server/index.ts`, `AppRuntime`, and `context.runtime`, while current code uses `app/server/app.ts`, `createAppContext`, and direct `context.`. + +**Why it matters:** docs are important to this project. Stale architecture guidance makes future changes less consistent and harder for agents/contributors to follow. + +**Suggested direction:** update the architecture docs to the current composition root and explicitly document exceptions such as config-file wrappers. + +## Go agent and WebSSH findings + +### 21. WebSSH WASM lacks a top-level disposal model and leaks JS functions + +**Severity:** High +**Area:** WebSSH lifecycle / browser resources + +**Evidence:** + +- `app/routes/ssh/page.tsx:156-188` creates the WASM/IPN instance and cleanup only sets `cancelled = true`. +- `app/routes/ssh/wasm.client.ts:27-29` exposes `openTunnel` only; no `dispose()`. +- `cmd/hp_ssh/hp_ssh.go:16-80` creates JS functions but never releases them with `js.Func.Release()`. +- `internal/hp_ipn/ipnserver.go:107-133` starts backend/server work but exposes no shutdown API. + +**Why it matters:** repeated SSH sessions can leave in-browser tsnet/backend resources and JS function handles alive until tab refresh. + +**Suggested direction:** add top-level `dispose()` to the JS API, release Go `js.Func` handles, cancel contexts, close sessions/backend/server, and call dispose from React cleanup. + +### 22. WebSSH disables SSH host key verification + +**Severity:** Medium/High +**Area:** Security / WebSSH + +**Evidence:** + +- `internal/hp_ipn/ssh.go:59-63` returns `nil` from `HostKeyCallback`. + +**Why it matters:** this accepts any host key and allows MITM within the network path. Tailscale identity reduces exposure, but SSH host identity is still bypassed. + +**Suggested direction:** document the tradeoff clearly at minimum. Prefer known_hosts-style pinning, Tailscale SSH identity integration, or an explicit trust-on-first-use flow. + +### 23. Go agent can panic on peers without Tailscale IPs + +**Severity:** Medium +**Area:** Go agent robustness + +**Evidence:** + +- `internal/tsnet/peers.go:61` indexes `peer.TailscaleIPs[0]`. +- `internal/tsnet/peers.go:119` also indexes `peer.TailscaleIPs[0]` before checking `len(ip) == 0`. + +**Why it matters:** transient or malformed peer state can crash host-info collection. + +**Suggested direction:** check `len(peer.TailscaleIPs) > 0` before indexing in both paths. + +### 24. Go agent preflight uses `http.Get` without timeout and does not close response bodies + +**Severity:** Medium +**Area:** Go agent robustness + +**Evidence:** + +- `internal/config/preflight.go:40-56` calls `http.Get(testURL)` directly. +- `internal/config/preflight.go:47-54` never closes `resp.Body`. + +**Why it matters:** startup can hang indefinitely on network issues, and response bodies leak. + +**Suggested direction:** use `http.Client{Timeout: ...}` and `defer resp.Body.Close()`. + +### 25. Go library-like code exits the process and logging is hand-rolled + +**Severity:** Medium +**Area:** Go maintainability + +**Evidence:** + +- `internal/tsnet/server.go:22-72` uses `log.Fatal` inside `NewAgent`/`Connect` rather than returning errors. +- `internal/util/logger.go:25-29` defines `encoder` and `pool`, but `internal/util/logger.go:59-66` only writes plain stderr lines and exits on fatal. + +**Why it matters:** callers cannot recover or return structured errors, and the logger has unused complexity while still lacking structured output. + +**Suggested direction:** return errors from `NewAgent`/`Connect`, let `cmd/hp_agent` decide process exit, and simplify or replace the logger. + +## Build, CI, and tooling findings + +### 26. CI does not explicitly run typecheck or lint + +**Severity:** Medium +**Area:** Tooling / quality gates + +**Evidence:** + +- `package.json` has `typecheck`, `lint`, and `format` scripts. +- `.github/workflows/build.yaml:32-37` runs `./build.sh --skip-pnpm-prune`, unit tests, and integration tests. +- `build.sh:157-173` runs `pnpm run build`, not `pnpm run typecheck` or `pnpm run lint`. + +**Why it matters:** Vite/React Router builds transpile TypeScript but are not a substitute for `tsgo` typechecking. Lint-only issues can land despite local scripts existing. + +**Suggested direction:** add `pnpm run typecheck` and `pnpm run lint` to CI. Consider `pnpm run format --check` if supported by oxfmt. + +### 27. Build script leaves temporary `vendor/` behind if WASM build fails + +**Severity:** Low/Medium +**Area:** Build hygiene + +**Evidence:** + +- `build.sh:143-154` runs `go mod vendor`, applies a patch, builds, then removes `vendor` only after success. + +**Why it matters:** failed local builds can leave a large generated directory in the worktree, which is easy to accidentally inspect or commit around. + +**Suggested direction:** add a trap around the vendoring step to remove `vendor` on failure. + +## Fate/SPA/Vite+ migration assessment + +### Updated direction: SPA first, not Void first + +After comparing the shape of Headplane with Void's current platform/runtime direction, the better target is **not** a Void app. Headplane is a self-hosted local control-plane UI that needs a predictable Node process, local filesystem/config access, SQLite, child process/agent management, long-lived SSE, and a packaging story that can eventually compile into a Node static SEA. + +The preferred target is now a one-way SPA cutover: + +```text +Hono Node server + Vite SPA + TanStack Router + raw Fate +``` + +The important architectural choice is that **Fate becomes the data framework directly** while the app shell stays thin. The shell should own static assets, cookies/session plumbing, routing, and lifecycle. Fate should own reads, mutations/actions, normalized cache updates, and live object/list subscriptions. Do not build a Headplane-specific abstraction layer over Fate's live bus, view resolution, actions, or native HTTP handlers unless a concrete repeated problem appears after using the raw APIs. + +Void may still be a useful reference implementation for Fate integration, but it should not drive Headplane's runtime architecture unless it later proves a first-class self-hosted Node mode that fits Headplane's install model. + +### Current Fate fit + +Fate is directly aimed at the pain Headplane is showing: declarative views, normalized cache, data masking, Async React, optimistic actions/mutations, and live views over SSE. Fate 1.0 says it now includes production-ready live views, Drizzle support, garbage collection, and native HTTP transport. The docs also still contain an alpha warning in the getting-started page, so I would treat the ecosystem as promising but still worth piloting behind a branch. + +Headplane is already close on prerequisites: + +- React is `19.2.5` and Fate requires React 19.2+. +- The app already uses Vite 8-era tooling, Vitest, Oxlint, Oxfmt, tsgo, and pnpm. +- Headplane already has Drizzle, but only for Headplane-local data (`users`, sessions, host info), while core tailnet data comes from the remote Headscale API. + +### What Fate would improve + +- Replace `LiveDataProvider` + `useRevalidator()` with object/list-level subscriptions (`useLiveView` / `useLiveListView`). +- Normalize `nodes`, `users`, pre-auth keys, and agent host info instead of passing large loader payloads around. +- Let components declare data needs near rendering rather than building large page-level loader DTOs. +- Make mutations return selected updated data and update dependent views without manual `context.hsLive.refresh(...)` calls. + +### What Fate will not fix + +- API-key-in-cookie session design. +- Self-linking authorization. +- Missing route/action capability checks. +- Headscale config-file write races. +- WebSSH/WASM lifecycle and SSH host-key verification. +- Go agent robustness issues. + +These should be fixed before or alongside any framework migration. + +### Headplane-specific adoption challenges + +- **Remote API source:** Fate's Drizzle adapter helps for local DB rows, but `Machine`, `User`, `PreAuthKey`, ACL policy, and DNS/config data mostly come from Headscale REST/config files. A Headscale Fate source/adapter or custom native HTTP query layer would be needed. +- **Authorization:** Fate views must be scoped by the authenticated principal. Do not reproduce the current process-global cache or mutable API client. +- **Live events:** Headscale does not appear to push the exact object-level events Headplane needs, so Headplane may still need polling or mutation-triggered `live.update(...)` calls. The win is to send object/list updates to subscribed views, not to revalidate whole routes. +- **Server framework:** the desired end state is no longer another heavyweight metaframework. Hono is a good fit for the server shell because it uses the Fetch `Request`/`Response` model Fate already targets, while still running as a normal self-hosted Node process. + +### Target runtime shape + +```text +╭────────────────────────────────────────────╮ +│ Node static SEA / Docker image │ +│ - bundled server JS │ +│ - embedded or adjacent Vite client assets │ +╰───────────────────┬────────────────────────╯ + │ + ▼ +╭────────────────────────────────────────────╮ +│ Hono Node server │ +│ - process lifecycle start/stop │ +│ - static asset + SPA fallback serving │ +│ - cookie/session middleware │ +│ - /fate and /fate/live │ +│ - /api, /events during transition │ +╰───────────────────┬────────────────────────╯ + │ + ▼ +╭────────────────────────────────────────────╮ +│ Framework-neutral Headplane server core │ +│ - auth/session/authorization │ +│ - Headscale API/config adapters │ +│ - agent manager │ +│ - Fate live bus used directly │ +╰───────────────────┬────────────────────────╯ + │ + ▼ +╭────────────────────────────────────────────╮ +│ Vite SPA │ +│ - TanStack Router for navigation/search │ +│ - raw Fate for all app data and mutations │ +│ - no route loaders/actions/fetchers │ +╰────────────────────────────────────────────╯ +``` + +### Node SEA packaging implications + +The SPA direction is a better fit for a Node static SEA than SSR framework mode because the runtime can become one server entry plus a finite static asset set. The server should be structured so that production can first serve assets from `build/client`, then later swap that out for an embedded asset manifest without changing application routing. + +Practical constraints for the SEA target: + +- keep one explicit production server entry instead of framework-generated adapter code; +- avoid dynamic runtime imports for route modules in production; +- make client assets addressable by a generated manifest rather than filesystem discovery; +- keep mutable data outside the SEA (`data_path`, Headscale config paths, SQLite, logs); +- preserve direct file serving for large WASM artifacts until we decide whether they should be embedded or adjacent assets. + +### Raw Fate rule + +Use Fate's public APIs directly: + +- `createFateServer(...)` and the native HTTP handler for `/fate`; +- Fate's live bus directly for `live.update(...)`, `live.delete(...)`, and connection/list invalidations; +- Fate's context callback directly for request auth and Headscale API access; +- `FateClient`, `createClient` / `createHTTPTransport` while bootstrapping, then generated `createFateClient(...)` once the Fate Vite plugin has a real server module; +- `useRequest`, `useView`, `useLiveView`, `useLiveListView`, and Fate actions directly in React. + +Do not add project-level wrappers such as `HeadplaneLivePublisher`, `HeadplaneDataContext`, or a custom Fate transport abstraction at the beginning. If raw Fate usage becomes repetitive, extract only the smallest local helper at the repetition site. + +### Current scaffold on `tale/fate-spa` + +- Removed the earlier `HeadplaneRuntime`, `HeadplaneDataContext`, and `HeadplaneLivePublisher` scaffolding. +- Added direct dependencies: `react-fate`, `@nkzw/fate`, `@tanstack/react-router`, `@tanstack/router-plugin`, and `@vitejs/plugin-react`. +- Added a plain Vite SPA `index.html` and `app/spa` entry with TanStack Router and a raw Fate client shell using `createClient` + `createHTTPTransport`. +- Moved the old React Router Vite config to `vite-old.config.ts` and replaced `vite.config.ts` with a clean SPA config. +- Added Hono and `@hono/node-server`, plus a minimal Hono server shell in `app/server/hono-app.ts`, `app/server/hono-dev.ts`, and `app/server/hono-main.ts`. +- `pnpm dev` now runs the Hono/Vite middleware shell with local `.data` storage for the example config; `pnpm build` now runs `vite build` for the SPA. +- Fate's Drizzle peer currently warns against the repo's Drizzle `1.0.0-beta.21`; avoid Fate's Drizzle adapter until that compatibility is resolved, and start with a direct Headscale source/resolver instead. + +### Recommended migration sequence + +1. **Remove transitional abstractions** and make the branch clearly one-way toward SPA + raw Fate. +2. **Install the direct dependencies**: `react-fate`, `@nkzw/fate`, `@tanstack/react-router`, `@tanstack/router-plugin`, and the plain Vite React plugin if the React Router plugin is removed. +3. **Replace the build/dev entry shape**: + - add a Vite SPA entry and TanStack route tree; + - stop generating new React Router route types; + - keep the existing Node server entry as the self-hosted process. +4. **Mount raw Fate endpoints** in the current Node request path: + - `/fate` for native RPC; + - `/fate/live` for SSE and subscription control; + - request context resolves auth using the existing auth service and calls `context.hsApi.getRuntimeClient(...)` directly. +5. **Convert the machines page first** using raw Fate views and live list/view hooks. +6. **Delete the old React Router loader/action/SSE path for converted data**, rather than running duplicate data models side-by-side. +7. **Fix critical auth/session issues early**: self-linking, raw API-key cookie, agent-route authz. +8. **Keep runtime changes minimal** until the SPA actually needs them: Hono routes, static SPA fallback, Fate routes, and later SEA asset serving. + +### Vite+ / VoidZero tooling assessment + +Vite+ is a unified CLI (`vp`) for Vite, Vitest, Oxlint, Oxfmt, Rolldown, tsdown, type checking, package-manager/runtime management, and task caching. Headplane already uses most of these tools separately, so Vite+ would mostly consolidate tooling and improve task ergonomics; it will not solve the data-layer problems by itself. + +Recommended Vite+ approach: + +- Try `vp migrate` in a separate branch only after adding CI typecheck/lint gates. +- Expect manual work around the custom React Router SSR entry and `runtime/vite-plugin.ts`. +- Do not combine Vite+ migration with Fate/Void/router migration in the same PR. + +## Suggested immediate backlog + +1. Fix OIDC self-linking authorization. +2. Make API-key sessions opaque/server-side and set explicit auth cookie flags. +3. Add capability checks to `/settings/agent` loader/action. +4. Fix live-data pause cleanup/refcounting. +5. Make `hsLive` use a stable server credential or principal-scoped cache. +6. Add raw Fate server/client dependencies and wire the Vite plugin. +7. Add the Vite SPA entry and TanStack Router skeleton. +8. Mount Fate's native `/fate` and `/fate/live` handlers directly. +9. Convert the machines page to raw Fate views/actions/live subscriptions. +10. Serialize config patches with a real mutex/queue and clone config arrays before editing. +11. Add CI `pnpm run typecheck` and `pnpm run lint`. +12. Add WebSSH top-level dispose and release Go `js.Func` values. diff --git a/app/server/hono-app.ts b/app/server/hono-app.ts new file mode 100644 index 0000000..c4bcfba --- /dev/null +++ b/app/server/hono-app.ts @@ -0,0 +1,107 @@ +import { versions } from "node:process"; + +import { serveStatic } from "@hono/node-server/serve-static"; +import { Hono, type Context } from "hono"; + +import type { AppContext } from "./context"; + +interface HonoAppOptions { + context: AppContext; + prefix: string; + staticRoot?: string; +} + +export function createHeadplaneHonoApp({ context, prefix, staticRoot }: HonoAppOptions) { + const app = new Hono(); + + const health = async (c: Context) => { + const api = context.hsApi.getRuntimeClient("fake-api-key"); + const healthy = await api.isHealthy(); + + return c.json({ status: healthy ? "OK" : "ERROR" }, healthy ? 200 : 500); + }; + + app.get("/healthz", health); + app.get(`${prefix}/healthz`, health); + + app.get(`${prefix}/api/info`, async (c) => { + if (context.config.server.info_secret == null) { + return c.json({ status: "Forbidden" }, 403); + } + + const bearer = c.req.header("Authorization") ?? ""; + if (!bearer.startsWith("Bearer ")) { + return c.json({ status: "Unauthorized" }, 401); + } + + const token = bearer.slice("Bearer ".length).trim(); + if (token !== context.config.server.info_secret) { + return c.json({ status: "Forbidden" }, 403); + } + + const api = context.hsApi.getRuntimeClient("fake-api-key"); + const healthy = await api.isHealthy(); + + return c.json({ + status: healthy ? "healthy" : "unhealthy", + headplane_version: __VERSION__, + headscale_canonical_version: healthy ? context.hsApi.apiVersion : "unknown", + internal_versions: { + node: versions.node, + v8: versions.v8, + uv: versions.uv, + zlib: versions.zlib, + openssl: versions.openssl, + libc: versions.libc, + }, + }); + }); + + app.get(`${prefix}/api/session`, async (c) => { + try { + const principal = await context.auth.require(c.req.raw); + + if (principal.kind === "api_key") { + return c.json({ + authenticated: true, + principal: { + kind: principal.kind, + sessionId: principal.sessionId, + displayName: principal.displayName, + }, + }); + } + + return c.json({ + authenticated: true, + principal: { + kind: principal.kind, + sessionId: principal.sessionId, + user: principal.user, + profile: principal.profile, + }, + }); + } catch { + return c.json({ authenticated: false }, 401); + } + }); + + app.all(`${prefix}/fate`, (c) => c.json({ error: "Fate server is not mounted yet" }, 501)); + app.all(`${prefix}/fate/*`, (c) => c.json({ error: "Fate server is not mounted yet" }, 501)); + + if (staticRoot) { + const stripPrefix = (path: string) => path.slice(prefix.length) || "/"; + + app.get(prefix, (c) => c.redirect(`${prefix}/`)); + app.use( + `${prefix}/*`, + serveStatic({ + root: staticRoot, + rewriteRequestPath: stripPrefix, + }), + ); + app.get(`${prefix}/*`, serveStatic({ root: staticRoot, path: "index.html" })); + } + + return app; +} diff --git a/app/server/hono-dev.ts b/app/server/hono-dev.ts new file mode 100644 index 0000000..ec044f8 --- /dev/null +++ b/app/server/hono-dev.ts @@ -0,0 +1,87 @@ +import { createServer } from "node:http"; +import { exit } from "node:process"; + +import { getRequestListener } from "@hono/node-server"; +import { createServer as createViteServer } from "vite"; + +import log from "~/utils/log"; + +import { ConfigError } from "./config/error"; +import { loadConfig } from "./config/load"; +import { createAppContext } from "./context"; +import { createHeadplaneHonoApp } from "./hono-app"; + +const PREFIX = process.env.__INTERNAL_PREFIX || "/admin"; +(globalThis as Record).__PREFIX__ = PREFIX; +(globalThis as Record).__VERSION__ = process.env.HEADPLANE_VERSION ?? "dev"; + +let config; +try { + config = await loadConfig(); +} catch (error) { + if (error instanceof ConfigError) { + log.error("server", "Unable to load configuration: %s", error.message); + } else { + log.error("server", "Failed to load configuration: %s", error); + } + exit(1); +} + +const context = await createAppContext(config); +context.auth.start(); + +const app = createHeadplaneHonoApp({ context, prefix: PREFIX }); +const honoListener = getRequestListener(app.fetch); +const vite = await createViteServer({ + appType: "spa", + server: { + middlewareMode: true, + }, +}); + +const server = createServer((req, res) => { + if (shouldUseHono(req.url)) { + void honoListener(req, res); + return; + } + + vite.middlewares(req, res, (error?: unknown) => { + if (error) { + if (error instanceof Error) { + vite.ssrFixStacktrace(error); + } + + log.error("server", "Vite middleware failed: %s", error); + res.statusCode = 500; + res.end("Internal Server Error"); + return; + } + + void honoListener(req, res); + }); +}); + +server.listen(config.server.port, config.server.host, () => { + log.info("server", "Listening on http://%s:%s", config.server.host, config.server.port); +}); + +function shouldUseHono(rawUrl: string | undefined) { + if (!rawUrl) { + return false; + } + + let pathname; + try { + pathname = new URL(rawUrl, "http://localhost").pathname; + } catch { + return false; + } + + return ( + pathname === "/healthz" || + pathname === `${PREFIX}/healthz` || + pathname === `${PREFIX}/fate` || + pathname.startsWith(`${PREFIX}/fate/`) || + pathname.startsWith(`${PREFIX}/api/`) + ); +} diff --git a/app/server/hono-main.ts b/app/server/hono-main.ts new file mode 100644 index 0000000..a1c315d --- /dev/null +++ b/app/server/hono-main.ts @@ -0,0 +1,46 @@ +import { exit } from "node:process"; + +import { serve } from "@hono/node-server"; + +import log from "~/utils/log"; + +import { ConfigError } from "./config/error"; +import { loadConfig } from "./config/load"; +import { createAppContext } from "./context"; +import { createHeadplaneHonoApp } from "./hono-app"; + +const PREFIX = process.env.__INTERNAL_PREFIX || "/admin"; +(globalThis as Record).__PREFIX__ = PREFIX; +(globalThis as Record).__VERSION__ = process.env.HEADPLANE_VERSION ?? "dev"; + +let config; +try { + config = await loadConfig(); +} catch (error) { + if (error instanceof ConfigError) { + log.error("server", "Unable to load configuration: %s", error.message); + } else { + log.error("server", "Failed to load configuration: %s", error); + } + exit(1); +} + +const context = await createAppContext(config); +context.auth.start(); + +const app = createHeadplaneHonoApp({ + context, + prefix: PREFIX, + staticRoot: "build/client", +}); + +serve( + { + fetch: app.fetch, + hostname: config.server.host, + port: config.server.port, + }, + (info) => { + log.info("server", "Listening on http://%s:%s", info.address, info.port); + }, +); diff --git a/app/spa/app.tsx b/app/spa/app.tsx new file mode 100644 index 0000000..f5bf977 --- /dev/null +++ b/app/spa/app.tsx @@ -0,0 +1,21 @@ +import { RouterProvider } from "@tanstack/react-router"; +import { FateClient, createClient, createHTTPTransport } from "react-fate"; + +import { router } from "./router"; + +const fate = createClient({ + roots: {}, + transport: createHTTPTransport({ + fetch: (input, init) => fetch(input, { ...init, credentials: "include" }), + url: `${__PREFIX__}/fate`, + }), + types: [], +}); + +export function App() { + return ( + + + + ); +} diff --git a/app/spa/main.tsx b/app/spa/main.tsx new file mode 100644 index 0000000..e210dac --- /dev/null +++ b/app/spa/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import "~/tailwind.css"; +import { App } from "./app"; + +const root = document.getElementById("root"); +if (!root) { + throw new Error("Unable to find root element"); +} + +createRoot(root).render( + + + , +); diff --git a/app/spa/router.tsx b/app/spa/router.tsx new file mode 100644 index 0000000..42c6ed6 --- /dev/null +++ b/app/spa/router.tsx @@ -0,0 +1,69 @@ +import { Link, Outlet, createRootRoute, createRoute, createRouter } from "@tanstack/react-router"; + +const rootRoute = createRootRoute({ + component: RootLayout, +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + component: HomePage, +}); + +const machinesRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/machines", + component: MachinesPage, +}); + +const routeTree = rootRoute.addChildren([indexRoute, machinesRoute]); + +export const router = createRouter({ + basepath: __PREFIX__, + routeTree, +}); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +function RootLayout() { + return ( +
+
+ +
+ +
+ +
+
+ ); +} + +function HomePage() { + return ( +
+

Headplane SPA

+

+ This is the one-way SPA shell. Data should move to raw Fate views and actions. +

+
+ ); +} + +function MachinesPage() { + return ( +
+

Machines

+

+ First migration target: replace the React Router loader/action/SSE model with raw Fate. +

+
+ ); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..a05a307 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Headplane + + +
+ + + diff --git a/package.json b/package.json index c3088d9..2d6ec01 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "sideEffects": false, "scripts": { "preinstall": "npx only-allow pnpm", - "build": "react-router build", - "dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev", - "start": "node build/server/index.js", + "build": "vite build", + "dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml HEADPLANE_SERVER__DATA_PATH=./.data tsx app/server/hono-dev.ts", + "start": "tsx app/server/hono-main.ts", "typecheck": "react-router typegen && tsgo", "test:unit": "vitest run --project unit", "test:integration": "vitest run --project integration:*", @@ -28,15 +28,19 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.2.8", + "@hono/node-server": "^2.0.2", "@iconify/react": "^6.0.2", "@kubernetes/client-node": "^1.4.0", "@lezer/highlight": "^1.2.3", + "@nkzw/fate": "^1.0.2", "@react-router/node": "^7.14.0", "@readme/openapi-parser": "^6.0.1", + "@tanstack/react-router": "^1.170.3", "@uiw/react-codemirror": "4.25.9", "arktype": "^2.2.0", "clsx": "^2.1.1", "drizzle-orm": "1.0.0-beta.21", + "hono": "^4.12.19", "isbot": "5.1.37", "jose": "6.2.2", "js-yaml": "^4.1.1", @@ -47,6 +51,7 @@ "react-codemirror-merge": "4.25.9", "react-dom": "19.2.5", "react-error-boundary": "^6.1.1", + "react-fate": "^1.0.2", "react-router": "^7.14.0", "react-router-dom": "^7.14.0", "restty": "^0.1.35", @@ -59,11 +64,13 @@ "@react-router/dev": "^7.14.0", "@shopify/lang-jsonc": "^1.0.1", "@tailwindcss/vite": "^4.2.2", + "@tanstack/router-plugin": "^1.168.4", "@types/js-yaml": "^4.0.9", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@typescript/native-preview": "7.0.0-dev.20260410.1", + "@vitejs/plugin-react": "^6.0.2", "drizzle-kit": "1.0.0-beta.21", "lefthook": "^2.1.5", "oxfmt": "^0.44.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a6a48a..aa3159d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@fontsource-variable/inter': specifier: ^5.2.8 version: 5.2.8 + '@hono/node-server': + specifier: ^2.0.2 + version: 2.0.2(hono@4.12.19) '@iconify/react': specifier: ^6.0.2 version: 6.0.2(react@19.2.5) @@ -44,12 +47,18 @@ importers: '@lezer/highlight': specifier: ^1.2.3 version: 1.2.3 + '@nkzw/fate': + specifier: ^1.0.2 + version: 1.0.2(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2))(drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@react-router/node': specifier: ^7.14.0 version: 7.14.0(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) '@readme/openapi-parser': specifier: ^6.0.1 version: 6.0.1(openapi-types@12.1.3) + '@tanstack/react-router': + specifier: ^1.170.3 + version: 1.170.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@uiw/react-codemirror': specifier: 4.25.9 version: 4.25.9(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.18.2(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.41.0)(@lezer/common@1.5.2))(@codemirror/language@6.12.3)(@codemirror/lint@6.8.2)(@codemirror/search@6.5.7)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.41.0)(codemirror@6.0.1(@lezer/common@1.5.2))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -61,7 +70,10 @@ importers: version: 2.1.1 drizzle-orm: specifier: 1.0.0-beta.21 - version: 1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2)) + version: 1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3) + hono: + specifier: ^4.12.19 + version: 4.12.19 isbot: specifier: 5.1.37 version: 5.1.37 @@ -92,6 +104,9 @@ importers: react-error-boundary: specifier: ^6.1.1 version: 6.1.1(react@19.2.5) + react-fate: + specifier: ^1.0.2 + version: 1.0.2(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2))(drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) react-router: specifier: ^7.14.0 version: 7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -116,13 +131,16 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.14.0 - version: 7.14.0(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + version: 7.14.0(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@shopify/lang-jsonc': specifier: ^1.0.1 version: 1.0.1 '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tanstack/router-plugin': + specifier: ^1.168.4 + version: 1.168.4(@tanstack/react-router@1.170.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -138,6 +156,9 @@ importers: '@typescript/native-preview': specifier: 7.0.0-dev.20260410.1 version: 7.0.0-dev.20260410.1 + '@vitejs/plugin-react': + specifier: ^6.0.2 + version: 6.0.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) drizzle-kit: specifier: 1.0.0-beta.21 version: 1.0.0-beta.21 @@ -170,13 +191,13 @@ importers: version: 6.0.2 vite: specifier: ^8.0.8 - version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vitepress: specifier: next - version: 2.0.0-alpha.12(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.9)(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 2.0.0-alpha.12(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(postcss@8.5.9)(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -998,6 +1019,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@2.0.2': + resolution: {integrity: sha512-tXlTi1h/4V7sDe7i97IVP+9re9ZU7wXZZggnR5ucCRclf1+AX6YhGStrR5w8bLj+3Mlyl0pKfBh9gqTqqnGKfQ==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@humanwhocodes/momoa@2.0.4': resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} @@ -1151,6 +1178,19 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@nkzw/fate@1.0.2': + resolution: {integrity: sha512-ouo5T9Vrn47nRypJJo1UeiSDPMlfP2JKB8Ge0WqJXZJmyjS60PbIn/UmEJNSwgYlVx6vu56RxUdYzUf54i1Xwg==} + hasBin: true + peerDependencies: + '@trpc/client': ^11.6.0 + drizzle-orm: ^0.45.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + vite: + optional: true + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} @@ -1594,6 +1634,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.46.3': resolution: {integrity: sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==} cpu: [arm] @@ -1814,9 +1857,79 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/history@1.162.0': + resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} + engines: {node: '>=20.19'} + + '@tanstack/react-router@1.170.3': + resolution: {integrity: sha512-WG3CZIMUmwL85aCswazWVgGoGkzSNUxAT66BraJY02Jrjs3mLSBHHNqoZuuAHUeH5MnakTKPva8k974+x6YseQ==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.171.1': + resolution: {integrity: sha512-ItrosN74HMhp6ia12rhw7V0vH85/SWHBupvTas/YN/fKh8bDQbiLntc0L/CcBaUilCyJANVkFkvEnFMKcibocg==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.4': + resolution: {integrity: sha512-l/u/AdJq4OPn7ejsOQS+DqER+ZQ4anZVFWJpVoqg1RHVtky844aLSnl4rcRM2BRYXZYBCtVyZBoJwsOEkSoinQ==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.4': + resolution: {integrity: sha512-2yihAaGv7U/ZcTwiSk3FPoLmlhYIFVfa7IGYaE5HRRAk96qiW+DxGtbzolp7sVX3Bw1t8yyKj6qljqOSgKZ6RA==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.3 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.0': + resolution: {integrity: sha512-c3GhqhBRCP636B41nf3TKvVz8EWzC5PTZ3I4J4LDH2tVjpxbyFNYsQKRtbNWiMFl+GTtgK4nCha346Wv7j4hcQ==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + '@tediousjs/connection-string@0.5.0': resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==} + '@trpc/client@11.17.0': + resolution: {integrity: sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg==} + hasBin: true + peerDependencies: + '@trpc/server': 11.17.0 + typescript: '>=5.7.2' + + '@trpc/server@11.17.0': + resolution: {integrity: sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw==} + hasBin: true + peerDependencies: + typescript: '>=5.7.2' + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1974,6 +2087,19 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + '@vitejs/plugin-vue@6.0.1': resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2143,6 +2269,14 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@4.3.0: + resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -2279,6 +2413,10 @@ packages: better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2294,6 +2432,10 @@ packages: brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2355,6 +2497,10 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2404,6 +2550,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -2412,6 +2561,10 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2504,6 +2657,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + docker-compose@1.4.2: resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} engines: {node: '>= 6.0.0'} @@ -2763,6 +2920,10 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + focus-trap@7.6.5: resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==} @@ -2818,6 +2979,10 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2848,6 +3013,10 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.12.19: + resolution: {integrity: sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -2887,20 +3056,36 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} hasBin: true + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2909,6 +3094,10 @@ packages: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -2935,6 +3124,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} @@ -3374,6 +3567,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -3451,6 +3648,13 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + react-fate@1.0.2: + resolution: {integrity: sha512-rXSX2h9fh7+xvuS3IEugQiSMmb6ncIX1kG+YAMSqNoEkLXormGjAAfjYKbZJoBuApmfgjQ+YDW9iiiHg2IBy3A==} + hasBin: true + peerDependencies: + react: ^19.2.0 + react-dom: ^19.2.0 + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3490,6 +3694,10 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -3566,6 +3774,16 @@ packages: engines: {node: '>=10'} hasBin: true + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -3691,6 +3909,10 @@ packages: resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -3788,6 +4010,10 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -3849,6 +4075,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -3873,6 +4103,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true valibot@1.3.1: @@ -4051,6 +4282,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4127,6 +4361,12 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4888,6 +5128,10 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 + '@hono/node-server@2.0.2(hono@4.12.19)': + dependencies: + hono: 4.12.19 + '@humanwhocodes/momoa@2.0.4': {} '@iconify-json/simple-icons@1.2.48': @@ -5080,6 +5324,15 @@ snapshots: '@neon-rs/load@0.0.4': optional: true + '@nkzw/fate@1.0.2(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2))(drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@trpc/client': 11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2) + superjson: 2.2.6 + zod: 4.4.3 + optionalDependencies: + drizzle-orm: 1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + '@oxc-project/types@0.124.0': {} '@oxfmt/binding-android-arm-eabi@0.44.0': @@ -5240,7 +5493,7 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@react-router/dev@7.14.0(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': + '@react-router/dev@7.14.0(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -5270,8 +5523,8 @@ snapshots: semver: 7.7.4 tinyglobby: 0.2.16 valibot: 1.3.1(typescript@6.0.2) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: @@ -5373,6 +5626,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.46.3': optional: true @@ -5539,15 +5794,101 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + + '@tanstack/history@1.162.0': {} + + '@tanstack/react-router@1.170.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/history': 1.162.0 + '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-core': 1.171.1 + isbot: 5.1.37 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/react-store@0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + + '@tanstack/router-core@1.171.1': + dependencies: + '@tanstack/history': 1.162.0 + cookie-es: 3.1.1 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + '@tanstack/router-generator@1.167.4': + dependencies: + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.1 + '@tanstack/router-utils': 1.162.0 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.8.2 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.4(@tanstack/react-router@1.170.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.1 + '@tanstack/router-generator': 1.167.4 + '@tanstack/router-utils': 1.162.0 + '@tanstack/virtual-file-routes': 1.162.0 + chokidar: 3.6.0 + unplugin: 3.0.0 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.170.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.162.0': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + ansis: 4.3.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.3': {} + + '@tanstack/virtual-file-routes@1.162.0': {} '@tediousjs/connection-string@0.5.0': {} + '@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2)': + dependencies: + '@trpc/server': 11.17.0(typescript@6.0.2) + typescript: 6.0.2 + + '@trpc/server@11.17.0(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5731,10 +6072,15 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@6.0.1(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.18(typescript@6.0.2))': + '@vitejs/plugin-react@6.0.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + + '@vitejs/plugin-vue@6.0.1(vite@7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.18(typescript@6.0.2))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.18(typescript@6.0.2) '@vitest/expect@4.1.4': @@ -5746,13 +6092,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.4': dependencies: @@ -5901,6 +6247,13 @@ snapshots: ansi-styles@6.2.3: {} + ansis@4.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -6039,6 +6392,8 @@ snapshots: prebuild-install: 7.1.3 optional: true + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -6063,6 +6418,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.17 @@ -6119,6 +6478,18 @@ snapshots: character-entities-legacy@3.0.0: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -6174,12 +6545,18 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@3.1.1: {} + cookie@1.1.1: {} copy-anything@3.0.5: dependencies: is-what: 4.1.16 + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + core-util-is@1.0.3: {} cpu-features@0.0.10: @@ -6253,6 +6630,8 @@ snapshots: dependencies: dequal: 2.0.3 + diff@8.0.4: {} + docker-compose@1.4.2: dependencies: yaml: 2.8.3 @@ -6286,7 +6665,7 @@ snapshots: get-tsconfig: 4.13.7 jiti: 2.6.1 - drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2)): + drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3): dependencies: '@types/mssql': 9.1.9(@azure/core-client@1.10.1) mssql: 11.0.1(@azure/core-client@1.10.1) @@ -6296,6 +6675,7 @@ snapshots: arktype: 2.2.0 better-sqlite3: 11.10.0 valibot: 1.3.1(typescript@6.0.2) + zod: 4.4.3 dunder-proto@1.0.1: dependencies: @@ -6478,6 +6858,10 @@ snapshots: file-uri-to-path@1.0.0: optional: true + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + focus-trap@7.6.5: dependencies: tabbable: 6.2.0 @@ -6542,6 +6926,10 @@ snapshots: github-from-package@0.0.0: optional: true + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -6583,6 +6971,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hono@4.12.19: {} + hookable@5.5.3: {} hpagent@1.2.0: {} @@ -6620,18 +7010,32 @@ snapshots: ip-address@10.1.0: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 + is-number@7.0.0: {} + is-stream@2.0.1: {} is-what@4.1.16: {} + is-what@5.5.0: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -6654,6 +7058,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jose@6.2.2: {} js-base64@3.7.8: @@ -7083,6 +7489,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pkg-types@2.3.0: @@ -7196,6 +7604,16 @@ snapshots: dependencies: react: 19.2.5 + react-fate@1.0.2(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2))(drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@nkzw/fate': 1.0.2(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@6.0.2))(typescript@6.0.2))(drizzle-orm@1.0.0-beta.21(@libsql/client@0.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(@types/better-sqlite3@7.6.13)(@types/mssql@9.1.9(@azure/core-client@1.10.1))(arktype@2.2.0)(better-sqlite3@11.10.0)(mssql@11.0.1(@azure/core-client@1.10.1))(valibot@1.3.1(typescript@6.0.2))(zod@4.4.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + transitivePeerDependencies: + - '@trpc/client' + - drizzle-orm + - vite + react-refresh@0.14.2: {} react-router-dom@7.14.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): @@ -7242,6 +7660,10 @@ snapshots: dependencies: minimatch: 5.1.9 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + readdirp@4.1.2: {} regex-recursion@6.0.2: @@ -7335,6 +7757,12 @@ snapshots: semver@7.7.4: {} + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + set-cookie-parser@2.7.2: {} shebang-command@2.0.0: @@ -7483,6 +7911,10 @@ snapshots: dependencies: copy-anything: 3.0.5 + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + tabbable@6.2.0: {} tabbable@6.4.0: {} @@ -7659,6 +8091,10 @@ snapshots: tmp@0.2.5: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tr46@0.0.3: {} trim-lines@3.0.1: {} @@ -7718,6 +8154,12 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -7753,13 +8195,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@3.2.4(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -7774,7 +8216,7 @@ snapshots: - tsx - yaml - vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.4) @@ -7785,13 +8227,13 @@ snapshots: optionalDependencies: '@types/node': 25.6.0 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -7802,12 +8244,12 @@ snapshots: '@types/node': 25.6.0 esbuild: 0.27.3 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 - vitepress@2.0.0-alpha.12(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.9)(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + vitepress@2.0.0-alpha.12(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(postcss@8.5.9)(terser@5.39.0)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: '@docsearch/css': 4.0.0-beta.7 '@docsearch/js': 4.0.0-beta.7 @@ -7816,7 +8258,7 @@ snapshots: '@shikijs/transformers': 3.9.2 '@shikijs/types': 3.9.2 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 6.0.1(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.18(typescript@6.0.2)) + '@vitejs/plugin-vue': 6.0.1(vite@7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.18(typescript@6.0.2)) '@vue/devtools-api': 8.0.0 '@vue/shared': 3.5.18 '@vueuse/core': 13.7.0(vue@3.5.18(typescript@6.0.2)) @@ -7825,7 +8267,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.1.2 shiki: 3.9.2 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.18(typescript@6.0.2) optionalDependencies: postcss: 8.5.9 @@ -7854,10 +8296,10 @@ snapshots: - universal-cookie - yaml - vitest@4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@types/node@25.6.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -7874,7 +8316,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 @@ -7898,6 +8340,8 @@ snapshots: webidl-conversions@3.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -7965,4 +8409,8 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod@3.25.76: {} + + zod@4.4.3: {} + zwitch@2.0.4: {} diff --git a/vite-old.config.ts b/vite-old.config.ts new file mode 100644 index 0000000..6a3029e --- /dev/null +++ b/vite-old.config.ts @@ -0,0 +1,90 @@ +import { execSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; + +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import { parse } from "yaml"; + +import { headplaneDevServer } from "./runtime/vite-plugin"; + +const PROD_ENTRY = "./app/server/main.ts"; +const DEV_ENTRY = "./app/server/app.ts"; + +const PREFIX = process.env.__INTERNAL_PREFIX || "/admin"; +if (PREFIX.endsWith("/")) { + throw new Error("Prefix must not end with a slash"); +} + +// Derive version: HEADPLANE_VERSION env > git describe > package.json +const isNext = process.env.IMAGE_TAG?.includes("next"); +let VERSION: string; +if (process.env.HEADPLANE_VERSION) { + VERSION = process.env.HEADPLANE_VERSION; +} else { + try { + const describe = execSync("git describe --tags", { encoding: "utf-8" }) + .trim() + .replace(/^v/, ""); + const tag = execSync("git describe --tags --abbrev=0", { encoding: "utf-8" }) + .trim() + .replace(/^v/, ""); + VERSION = describe === tag ? tag : `${tag}-dev+${describe.split("-").pop()}`; + } catch { + const pkg = await readFile("package.json", "utf-8"); + VERSION = JSON.parse(pkg).version; + } +} + +if (!VERSION) { + throw new Error("Unable to determine version"); +} + +// Load the config without any environment variables (not needed here) +const config = await readFile("config.example.yaml", "utf-8"); +const { server } = parse(config); + +export default defineConfig(({ command, isSsrBuild }) => ({ + base: command === "build" ? `${PREFIX}/` : undefined, + plugins: [ + headplaneDevServer({ + entry: DEV_ENTRY, + basename: PREFIX, + publicDir: new URL("./public", import.meta.url).pathname, + }), + reactRouter(), + tailwindcss(), + ], + server: { + host: server.host, + port: server.port, + }, + resolve: { + tsconfigPaths: true, + }, + build: { + target: "baseline-widely-available", + sourcemap: true, + rollupOptions: + command === "build" + ? { + // Override the SSR build entry so React Router emits the + // production bootstrap (`app/server/main.ts`) as + // `build/server/index.js`. It transitively imports the + // SSR entry, which pulls in the React Router server build + // via the virtual module `virtual:react-router/server-build`. + input: isSsrBuild ? PROD_ENTRY : undefined, + + // Exclude WASM from the client since it fetches from the server + external: isSsrBuild ? [] : [/\.wasm(\?url)?$/], + } + : undefined, + }, + ssr: { + noExternal: command === "build" ? true : undefined, + }, + define: { + __VERSION__: JSON.stringify(isNext ? `${VERSION}-next` : VERSION), + __PREFIX__: JSON.stringify(PREFIX), + }, +})); diff --git a/vite.config.ts b/vite.config.ts index 6a3029e..cc327db 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,22 +1,17 @@ import { execSync } from "node:child_process"; import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; -import { reactRouter } from "@react-router/dev/vite"; import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import { parse } from "yaml"; -import { headplaneDevServer } from "./runtime/vite-plugin"; - -const PROD_ENTRY = "./app/server/main.ts"; -const DEV_ENTRY = "./app/server/app.ts"; - const PREFIX = process.env.__INTERNAL_PREFIX || "/admin"; if (PREFIX.endsWith("/")) { throw new Error("Prefix must not end with a slash"); } -// Derive version: HEADPLANE_VERSION env > git describe > package.json const isNext = process.env.IMAGE_TAG?.includes("next"); let VERSION: string; if (process.env.HEADPLANE_VERSION) { @@ -40,51 +35,33 @@ if (!VERSION) { throw new Error("Unable to determine version"); } -// Load the config without any environment variables (not needed here) const config = await readFile("config.example.yaml", "utf-8"); const { server } = parse(config); -export default defineConfig(({ command, isSsrBuild }) => ({ - base: command === "build" ? `${PREFIX}/` : undefined, - plugins: [ - headplaneDevServer({ - entry: DEV_ENTRY, - basename: PREFIX, - publicDir: new URL("./public", import.meta.url).pathname, - }), - reactRouter(), - tailwindcss(), - ], +export default defineConfig({ + appType: "spa", + base: `${PREFIX}/`, + plugins: [react(), tailwindcss()], server: { host: server.host, port: server.port, }, resolve: { - tsconfigPaths: true, + alias: { + "~": fileURLToPath(new URL("./app", import.meta.url)), + }, }, build: { - target: "baseline-widely-available", + emptyOutDir: true, + outDir: "build/client", sourcemap: true, - rollupOptions: - command === "build" - ? { - // Override the SSR build entry so React Router emits the - // production bootstrap (`app/server/main.ts`) as - // `build/server/index.js`. It transitively imports the - // SSR entry, which pulls in the React Router server build - // via the virtual module `virtual:react-router/server-build`. - input: isSsrBuild ? PROD_ENTRY : undefined, - - // Exclude WASM from the client since it fetches from the server - external: isSsrBuild ? [] : [/\.wasm(\?url)?$/], - } - : undefined, - }, - ssr: { - noExternal: command === "build" ? true : undefined, + target: "baseline-widely-available", + rollupOptions: { + external: [/\.wasm(\?url)?$/], + }, }, define: { __VERSION__: JSON.stringify(isNext ? `${VERSION}-next` : VERSION), __PREFIX__: JSON.stringify(PREFIX), }, -})); +});