feat(web): vercel deployment and vds compose/systemd setup

Add Vercel config with React Router preset, vercel-build script and
/api/auth rewrite to the Convex site. Add VDS staging compose for the
Rivet engine and runner plus systemd units, deployment plan, and
Docker/Vercel ignore rules. Lockfile covers both the web and agents
dependency additions.
This commit is contained in:
-Puter
2026-08-01 20:17:59 +05:30
parent 4dc878b8cb
commit 08bc5ae259
17 changed files with 601 additions and 1 deletions

35
.dockerignore Normal file
View File

@@ -0,0 +1,35 @@
# Repo-wide ignore for the agents/runner Docker builds.
# Keeps images small and prevents secrets from entering image layers.
node_modules
**/node_modules
**/dist
**/.flue-vite
# Secrets — never bake into an image.
.env
.env.*
!.env.example
**/.env
**/.env.*
# Local Convex / runtime state.
.convex
**/.convex
data
**/data
# VCS, caches, and editor cruft.
.git
.gitignore
**/.DS_Store
**/.vscode
**/.idea
# macOS AppleDouble sidecars are metadata, never TypeScript source.
**/._*
# Workspaces and runner artifacts created at runtime on the host.
workspaces
**/workspaces
*.log

21
.gitignore vendored
View File

@@ -55,3 +55,24 @@ coverage
tmp
temp
.env.*
.env*
infra/ansible/.gitignore
infra/ansible/ansible.cfg
infra/ansible/README.md
infra/ansible/requirements.yml
infra/ansible/group_vars/staging_vds.example/main.yml
infra/ansible/group_vars/staging_vds.example/vault.yml
infra/ansible/inventory/hosts.example.yml
infra/ansible/playbooks/converge.yml
infra/ansible/roles/backup/handlers/main.yml
infra/ansible/roles/backup/tasks/main.yml
infra/ansible/roles/backup/templates/zopu-backup.service.j2
infra/ansible/roles/backup/templates/zopu-backup.sh.j2
infra/ansible/roles/backup/templates/zopu-backup.timer.j2
infra/ansible/roles/common/tasks/main.yml
infra/ansible/roles/directories/tasks/main.yml
infra/ansible/roles/docker/handlers/main.yml
infra/ansible/roles/docker/tasks/main.yml
infra/ansible/roles/docker/vars/main.yml
infra/ansible/roles/firewall/handlers/main.yml
infra/ansible/roles/firewall/tasks/main.yml

17
.vercelignore Normal file
View File

@@ -0,0 +1,17 @@
# Archived implementations and agent reference material are not part of the web build.
repos/
.agents/
.codex/
# Private deployment operations are deployed to the VDS, not Vercel.
deploy/
infra/
docs/
# Local development state and tooling.
.vscode/
coverage/
.env
.env.*
packages/backend/.convex/
.vercel/output/

View File

@@ -35,6 +35,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vercel/react-router": "1.3.1",
"react-router-devtools": "^6.2.1",
"tailwindcss": "catalog:",
"typescript": "catalog:",

View File

@@ -1,7 +1,8 @@
import type { Config } from "@react-router/dev/config";
import { vercelPreset } from "@vercel/react-router/vite";
export default {
appDirectory: "src",
presets: [vercelPreset()],
ssr: true,
} satisfies Config;

63
deploy/compose/Caddyfile Normal file
View File

@@ -0,0 +1,63 @@
# Caddy reverse proxy for the Zopu VDS staging agent stack.
#
# This is the ONLY public ingress to the private worker (see
# docs/DEPLOYMENT_PLAN.md §"Public surface"). It terminates TLS for the Flue
# callback hostname that Convex reaches, then forwards ONLY the documented agent
# worker paths to the internal `agents` service.
#
# Hard rules this file enforces:
# - No generic /api/* proxy and no browser-to-Flue traffic (DEPLOYMENT_PLAN.md
# line 86). Browsers talk only to Convex.
# - Only the Flue Node worker paths Convex actually calls are routed:
# /agents/zopu/<organizationId> conversation admission
# /internal/work-attempts/* work attempt execute + cancel
# /api/rivet/* RivetKit Actor gateway surface
# /workflows/* Flue workflow execution
# /health liveness probe (Compose healthcheck/CI)
# - Everything else returns 404. The private worker protocol stays narrow.
#
# Variables are injected by the `caddy` Compose service environment:
# {$AGENTS_HOST} public hostname, e.g. agents-staging.example.com
# {$AGENTS_UPSTREAM} internal upstream host:port, e.g. agents:3000
# {$ACME_EMAIL} email for the Let's Encrypt account
#
# TLS certs and the ACME account persist in the caddy-data volume. Caddy serves
# its own ACME HTTP-01 challenge responses on :80 automatically, so the :80
# block below only redirects everything else to HTTPS — worker traffic is never
# served over plain HTTP.
{
email {$ACME_EMAIL}
}
# --- TLS termination for the Flue callback hostname ---------------------------
{$AGENTS_HOST} {
encode zstd gzip
# Conversation admission: Convex POSTs to /agents/zopu/<organizationId>.
# The conversationRoute middleware requires the FLUE_DB_TOKEN bearer and the
# matching x-zopu-organization-id header.
reverse_proxy /agents/zopu/* {$AGENTS_UPSTREAM}
# Internal work-attempt execute/cancel. Requires the internalRoute bearer.
reverse_proxy /internal/work-attempts/* {$AGENTS_UPSTREAM}
# RivetKit Actor gateway surface (app.all("/api/rivet/*")). Routed so the
# runtime registry handler is reachable for actor metadata/start.
reverse_proxy /api/rivet/* {$AGENTS_UPSTREAM}
# Flue workflow execution (workflows/plan-work). Requires the workflowRoute
# bearer + x-zopu-organization-id header.
reverse_proxy /workflows/* {$AGENTS_UPSTREAM}
# Caddy routes the liveness request to the actual worker; it must not mask a
# failed worker process with a synthetic successful response.
reverse_proxy /health {$AGENTS_UPSTREAM}
# Everything else is not part of the private worker protocol.
respond 404
}
# --- HTTP → HTTPS redirect (Caddy still answers ACME challenges on :80) -------
:80 {
redir https://{host}{uri} permanent
}

View File

@@ -0,0 +1,56 @@
# Zopu VDS staging — Rivet engine + runner on host network.
#
# The agents registry and Caddy run on the HOST via systemd (not Docker).
# This compose only manages the two Rivet services, both on host networking:
#
# engine — Rivet Engine (RocksDB backend), binds 127.0.0.1:6420 on the host.
# UFW (policy DROP, only 22/80/443 open) keeps it private.
# runner — AgentOS runner, reaches the engine at 127.0.0.1:6420.
#
# With network_mode: host there are no Docker bridge networks, no port
# forwarding, and no host.docker.internal gymnastics — both services share
# the host network namespace directly.
#
# Usage:
# docker compose --env-file .env up -d
services:
engine:
image: ${ENGINE_IMAGE:?ENGINE_IMAGE must be set}
restart: unless-stopped
network_mode: host
volumes:
- type: bind
source: ${ZOPU_DEPLOY_ROOT:?ZOPU_DEPLOY_ROOT is required}/data/rivet
target: /data
environment:
RIVET__FILE_SYSTEM__PATH: /data
RIVET__AUTH__ADMIN_TOKEN: ${RIVET_ADMIN_TOKEN:?RIVET_ADMIN_TOKEN is required}
RIVET_LOG_LEVEL: ${RIVET_LOG_LEVEL:-info}
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:6420/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
runner:
image: ${RUNNER_IMAGE:?RUNNER_IMAGE must be set}
restart: unless-stopped
network_mode: host
environment:
NODE_ENV: production
RIVET_RUNNER_VERSION: ${RIVET_RUNNER_VERSION:?RIVET_RUNNER_VERSION is required}
RIVET_ENVOY_VERSION: ${RIVET_RUNNER_VERSION:?RIVET_RUNNER_VERSION is required}
RIVET_ENDPOINT: http://${RIVET_NAMESPACE:-default}:${RIVET_ADMIN_TOKEN:?RIVET_ADMIN_TOKEN is required}@127.0.0.1:6420
RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN:?RIVET_WORKSPACE_TOKEN is required}
AGENT_WORKSPACE_ROOT: /var/lib/zopu/workspaces
BUN_EXECUTABLE: /usr/local/bin/bun
AGENT_MODEL_API_KEY: ${AGENT_MODEL_API_KEY:?AGENT_MODEL_API_KEY is required}
volumes:
- type: bind
source: ${ZOPU_DEPLOY_ROOT:?ZOPU_DEPLOY_ROOT is required}/workspaces
target: /var/lib/zopu/workspaces
depends_on:
engine:
condition: service_healthy

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://rivet.dev/engine-config-schema.json",
"file_system": {
"path": "/data"
},
"singleplayer": false
}

18
deploy/systemd/Caddyfile Normal file
View File

@@ -0,0 +1,18 @@
{
email ops@zopu.ai
}
{$AGENTS_HOST} {
encode zstd gzip
route {
reverse_proxy /agents/zopu/* 127.0.0.1:3000
reverse_proxy /internal/work-attempts/* 127.0.0.1:3000
reverse_proxy /api/rivet/* 127.0.0.1:3000
reverse_proxy /workflows/* 127.0.0.1:3000
reverse_proxy /health 127.0.0.1:3000
respond 404
}
}
:80 {
redir https://{host}{uri} permanent
}

View File

@@ -0,0 +1,23 @@
[Unit]
Description=Zopu agent registry
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target
[Service]
Type=simple
User=zopu
Group=zopu
WorkingDirectory=/srv/zopu/source/packages/agents
EnvironmentFile=/etc/zopu/agents.env
ExecStart=/opt/zopu/node/bin/node dist/server.mjs
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/srv/zopu/workspaces
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,4 @@
[Service]
EnvironmentFile=/etc/zopu/agents.env
ExecStart=
ExecStart=/usr/bin/caddy run --config /etc/caddy/Caddyfile

168
docs/DEPLOYMENT_PLAN.md Normal file
View File

@@ -0,0 +1,168 @@
# Deployment Architecture Recommendation
> **Status:** Proposed — research completed 2026-07-31. This supersedes the shared-Convex-environment approach in `docs/deployment.md` once implemented.
>
> **Goal:** retain an instant local iteration loop while operating one stable public staging environment with reproducible, declarative infrastructure.
## Decision
Use a deliberately split stack:
```text
Fast iteration (private development) Stable staging (public)
──────────────────────────────────── ────────────────────────
Mac + Tailscale Vercel + Contabo VDS
Web / Flue / Rivet / runner Web SSR Agents VDS
│ │ │
personal Convex dev deployment staging Convex deployment
```
| Concern | Iteration | Staging |
| --- | --- | --- |
| User-visible URL | Existing Tailscale URL on the Mac | `https://staging.<domain>` on Vercel |
| Product backend | Personal Convex **dev** deployment | Dedicated long-lived Convex deployment |
| Web | Vite HMR on the Mac | Vercel React Router SSR |
| Agent worker | Local Flue, engine, and runner | Contabo VDS: Flue, Rivet Engine, AgentOS runner |
| Delivery | Run local `pnpm dev:tailscale` | CI applies IaC then updates immutable images |
| State/credentials | Development-only | Independent staging secrets, OAuth app, and volumes |
**Do not create a second remote “fast iteration” stack.** It duplicates the slowest part of the loop—building and replacing container images—while the existing Mac/Tailscale stack already exercises the complete topology from a phone. Staging is the public link and integration gate; local development is the fast environment.
## Why this split
The repository has three materially different runtime needs:
1. **Web** is a React Router SSR Node application. Its current production artifact is `react-router build` plus `react-router-serve`, and `apps/web/Dockerfile` already serves it on Node. Vercel supports React Router SSR and streaming, and is the appropriate managed host for this stateless application. [Vercel React Router guide](https://vercel.com/docs/frameworks/frontend/react-router)
2. **Convex** owns authentication, durable product data, workflows, and reactive client projections. It is not an application container to run on the VDS. The architecture explicitly requires clients to communicate only with Convex. (`docs/TECH.md`, §1.)
3. **Flue + Rivet Engine + AgentOS runner** are persistent worker processes. The runner creates Git worktrees, installs dependencies, and host-mounts those directories into AgentOS; they require a long-lived filesystem and must stay co-located with their workspace volume. They are not appropriate for Vercel or Cloudflare Workers. [Flue Node Docker deployment](https://flueframework.com/docs/ecosystem/deploy/docker) · [Rivet runtime modes](https://rivet.dev/docs/general/runtime-modes)
Cloudflare remains valuable for authoritative DNS, TLS/DDoS controls, and optionally a later public-edge layer. It is **not** the first frontend runtime choice: moving the current Node SSR artifact to Workers requires a Cloudflare-specific React Router/workerd build and `nodejs_compat`; it does not host the private worker stack. [Cloudflare React Router guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/)
## Environment isolation — required, not optional
The present shared Convex deployment is incompatible with two simultaneously operating environments. `SITE_URL` is a single deployment-scoped Better Auth base/trusted origin and GitHub OAuth callback origin; switching it between hosts breaks the other host (`docs/deployment.md`, lines 1947).
Create these independent Convex deployments:
| Deployment | Type | Purpose |
| --- | --- | --- |
| `dev:<developer>` | Convex dev | Local loop only; each developer owns one |
| `staging` | Long-lived production-type deployment | Public integration/staging; never reused for local testing |
| Branch previews | Ephemeral preview deployment | Optional later, only for frontend/backend changes that do not need OAuth |
Convex supports a named long-lived production-type deployment (`convex deployment create staging --type prod`), per-deployment environment variables, and branch preview deployments. [Convex multiple deployments](https://docs.convex.dev/production/multiple-deployments) · [Convex environment variables](https://docs.convex.dev/production/environment-variables)
Every environment gets its own:
- `SITE_URL`, exact browser origin;
- `FLUE_URL` / `AGENT_BACKEND_URL`, pointing at only that environments Flue worker;
- `FLUE_DB_TOKEN` shared only with that environments agent service;
- model and Git provider credentials;
- GitHub OAuth application/client credentials where GitHub login is enabled.
A GitHub OAuth App permits one callback URL. Use a staging OAuth App with `https://staging.<domain>/api/auth/callback/github`; keep local development on its own OAuth app or login mechanism. Do not promise OAuth on throwaway Vercel preview domains. [GitHub OAuth Apps](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
## Staging topology
```mermaid
flowchart LR
Browser[Browser] -->|HTTPS| Vercel[Vercel: staging web SSR]
Browser -->|queries & mutations| Convex[Convex: staging]
Vercel -->|/api/auth rewrite| ConvexSite[Convex Site auth routes]
Convex -->|service-authenticated HTTPS| Flue[Contabo: Flue Node service]
Flue --> Engine[Rivet Engine: private Docker network]
Runner[AgentOS runner: private Docker network] --> Engine
Runner --> Volumes[Persistent source mirror + workspaces]
CF[Cloudflare DNS] --> Vercel
CF --> Flue
```
### Public surface
- `staging.<domain>` → Vercel.
- `agents-staging.<domain>` → Contabo Caddy/Traefik → Flue only.
- Only ports 80/443 (and Tailscale/managed SSH) enter the VDS. Do **not** publish Rivet ports `6420` or `6421`, the AgentOS runner, workspace directories, or any engine dashboard.
- The Flue route is reachable from Convex, but its middleware must continue to require the per-environment bearer `FLUE_DB_TOKEN` and organization/turn correlation headers. This is a private worker protocol exposed narrowly for Convex callbacks—not a browser API.
- Browser clients continue to talk only to Convex. Do not restore browser-to-Flue traffic or a generic `/api/*` agent proxy.
### Private VDS services
The checked-in Compose topology should contain exactly these services:
| Service | Network exposure | Persistent data | Notes |
| --- | --- | --- | --- |
| `engine` | Internal only | `/data` bind mount | Single-node RocksDB is suitable for staging. Set an admin token; probe `:6420/health`. |
| `agents` | Caddy upstream only | Shared workspace bind mount | Runs the existing Flue Node build and owns worktree preparation; `/health` is a liveness endpoint. |
| `runner` | Internal only | Shared workspace bind mount | Separate from `agents`; needs Bun, Git, and the same worktree paths prepared by `agents`. |
| `caddy` | 80/443 only | Caddy certificate/config bind mounts | TLS for the Flue callback hostname. |
Rivets filesystem backend is explicitly appropriate for single-node deployments; multi-node/HA later requires PostgreSQL and NATS. Configure the engine with a persistent `/data` bind mount, admin token, resource limits, and a `:6420/health` probe. [Rivet Docker Compose](https://rivet.dev/docs/self-hosting/docker-compose) · [Rivet production checklist](https://rivet.dev/docs/self-hosting/production-checklist)
The runner must receive a **build-time** `RIVET_RUNNER_VERSION` derived from the CI run or immutable release revision. Rivet uses it to route new actors to the new runner and drain old actors; without it, existing actors can continue on old code.
## IaC model: Pulumi + Ansible + Compose, each at the right seam
Dokploy is intentionally excluded: it has already proved too slow and imperative for this stack. One tool should not be forced to manage three different concerns poorly.
| Layer | Source of truth | Tool | Reason |
| --- | --- | --- | --- |
| SaaS control plane | `infra/pulumi` | **Pulumi TypeScript** | Declarative Cloudflare DNS and Vercel project/domain/environment configuration; one `staging` stack now, later `production`; encrypted stack secrets and `preview`/`refresh` drift workflows. |
| VDS baseline | `infra/ansible` | **Ansible** | Idempotent OS convergence: service user, Docker, firewall, Tailscale, directories, Caddy prerequisites, and backup timer. No Pulumi SSH-command pseudo-provider. |
| VDS application topology | `deploy/compose` | **Docker Compose** | Explicit services, networks, volumes, healthchecks, images, and restart policy in the repository. This is the deployable unit. |
| Release execution | CI | **Gitea Actions or an equivalent CI runner** | Builds tagged images, pushes them to a registry, runs `ansible-playbook`, then applies a pinned Compose release and checks health. |
Pulumi state is meaningful only with a shared backend—use Pulumi Cloud or a managed/self-hosted state backend, **not** a developer-local `file://` state file. Pulumis state is what enables previews, refreshes, encrypted secret tracking, and drift detection. [Pulumi state and backends](https://www.pulumi.com/docs/iac/concepts/state-and-backends/) · [Pulumi secrets](https://www.pulumi.com/docs/iac/concepts/secrets/)
Ansible is not redundant: it makes the Contabo host reproducible without pretending that SSH command resources are declarative infrastructure. [Ansible basic concepts](https://docs.ansible.com/projects/ansible/latest/getting_started/)
## Delivery workflow
### Local iteration
1. Keep using the current root `pnpm dev:tailscale` stack and personal Convex dev deployment.
2. Use the Macs Tailscale URL for phone testing.
3. Never edit staging Convex variables, staging OAuth callback settings, or staging VDS volumes from the local loop.
4. Merge only when targeted runtime smoke testing and repository checks pass.
### Staging release
1. A merge to the staging branch (initially `master` if that is the stable branch) starts CI.
2. CI runs type checks and targeted tests.
3. CI builds the web for the **staging Convex deployment** and deploys it to the dedicated staging Vercel project. Build-time `VITE_CONVEX_URL` and `VITE_AUTH_URL` must be staging values.
4. CI builds immutable `agents` and `runner` images tagged with commit SHA; it sets `RIVET_RUNNER_VERSION` from the release identity.
5. CI runs Ansible convergence, updates the Compose release to those exact image digests, and executes `docker compose up -d`.
6. CI verifies: Vercel URL returns 200, Convex auth origin is accepted, agents health endpoint returns 200, engine health returns 200 inside the private network, and a real signed-in staging conversation receives an agent response.
7. Rollback means redeploying the previous image digests and restoring the corresponding Vercel deployment—not rebuilding mutable `latest` images.
Do not couple the Vercel deployment to Gitea-native Git integration assumptions. The repository is hosted on Gitea, so start with CI invoking the Vercel CLI/API. If a Git mirror is later introduced, Vercel previews can be enabled separately.
## Required implementation backlog
This research does **not** deploy anything. Before the first staging release, complete these changes in order:
1. Create the separate `staging` Convex deployment and its deploy key; configure deployment-scoped secrets and `SITE_URL`.
2. Correct the auth/webhook origin seams:
- Add Vercel production rewrites for `/api/auth/*` to the staging Convex Site URL; Vites current proxy only applies to local development.
- Make the Puter webhook target the staging Convex Site HTTP action directly, rather than the web origin.
3. Add Vercel React Router support and a dedicated staging deployment configuration.
4. Create the four-service Compose topology and a runner-capable image. The current agents Dockerfile starts only Flue and does not provide the dedicated Bun/Git runner service.
5. Add health routes/checks, resource limits, volume backup, image-digest deployments, and `RIVET_RUNNER_VERSION`.
6. Add `infra/pulumi` and `infra/ansible`, then CI environments with protected staging secrets.
7. Execute an end-to-end staging smoke: sign in, create/connect a project, send a conversation, and complete a disposable issue-to-PR job.
## Security and operations guardrails
- Keep environment secrets in CI environment secret stores, Pulumi encrypted configuration/ESC where appropriate, Convex deployment variables, and Vercel environment variables. Never commit `.env` files or place secrets in image layers.
- Use unique `FLUE_DB_TOKEN`, Rivet admin token, workspace token, model credential, and Git token per environment.
- The VDS runs code-writing agents. Use a dedicated service user; do not mount the host home directory; expose only scoped repository credentials to individual attempts; and keep staging separate from any production host. This follows the repository-isolation policy in `docs/TECH.md` §11.
- Back up Rivet engine state and Caddy configuration. Treat workspaces as reproducible/ephemeral unless an active job requires retention; prune completed worktrees deliberately.
- A single Flue Node instance is the correct initial staging shape. Its durable Convex adapter survives restarts, but each conversation still requires one live owner—do not add replicas until ownership routing is designed. [Flue database guide](https://flueframework.com/docs/guide/database)
## Sources
- Repository architecture: `docs/TECH.md` §§1, 1011; `docs/LOCAL_SETUP.md`; `apps/web/Dockerfile`; `packages/agents/Dockerfile`; `packages/agents/src/runtime/repository-workspace.ts`; `packages/agents/src/runtime/attempt-runner.ts`.
- [Convex environments and deployments](https://docs.convex.dev/production/multiple-deployments), [hosting on Vercel](https://docs.convex.dev/production/hosting/vercel), and [HTTP actions](https://docs.convex.dev/functions/http-actions).
- [Vercel React Router](https://vercel.com/docs/frameworks/frontend/react-router) and [Vercel environments](https://vercel.com/docs/deployments/environments).
- [Rivet self-hosted Docker Compose](https://rivet.dev/docs/self-hosting/docker-compose), [production checklist](https://rivet.dev/docs/self-hosting/production-checklist), and [version upgrades](https://rivet.dev/docs/actors/versions).
- [Flue Node/Docker deployment](https://flueframework.com/docs/ecosystem/deploy/docker) and [durable database ownership](https://flueframework.com/docs/guide/database).
- [Pulumi state](https://www.pulumi.com/docs/iac/concepts/state-and-backends/), [Pulumi secrets](https://www.pulumi.com/docs/iac/concepts/secrets/), and [Ansible](https://docs.ansible.com/projects/ansible/latest/getting_started/).

View File

@@ -69,6 +69,7 @@
"build:agents": "vp run --filter @code/agents build",
"docs:update": "node scripts/update-docs.ts",
"subtree": "node scripts/subtree.ts",
"vercel-build": "node scripts/vercel-build.ts",
"check": "ultracite check --disable-nested-config",
"fix": "ultracite fix --disable-nested-config"
},
@@ -80,12 +81,14 @@
"@flue/sdk": "npm:@rivet-dev/labs-flue-sdk@1.0.0-beta.9-rivet.2",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@vercel/config": "^0.5.5",
"concurrently": "^9.1.2",
"convex": "catalog:",
"convex-test": "catalog:",
"effect": "catalog:",
"oxfmt": "0.61.0",
"oxlint": "1.76.0",
"react-router": "^8.1.0",
"rolldown": "1.1.4",
"typescript": "catalog:",
"ultracite": "7.9.3",

View File

@@ -145,6 +145,7 @@ export const list = query({
return connections.map((connection) => ({
connectedAt: connection.connectedAt,
credentialKind: connection.credentialKind,
grantedScopesJson: connection.grantedScopesJson,
id: String(connection._id),
lastVerifiedAt: connection.lastVerifiedAt,
provider: connection.provider,

150
pnpm-lock.yaml generated
View File

@@ -121,6 +121,9 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 22.20.1
'@vercel/config':
specifier: ^0.5.5
version: 0.5.5
concurrently:
specifier: ^9.1.2
version: 9.2.4
@@ -139,6 +142,9 @@ importers:
oxlint:
specifier: 1.76.0
version: 1.76.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(yaml@2.9.0))
react-router:
specifier: ^8.1.0
version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
rolldown:
specifier: 1.1.4
version: 1.1.4
@@ -227,6 +233,9 @@ importers:
'@types/react-dom':
specifier: 'catalog:'
version: 19.2.3(@types/react@19.2.17)
'@vercel/react-router':
specifier: 1.3.1
version: 1.3.1(@react-router/dev@8.3.0(@react-router/serve@8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2)(wrangler@4.114.0))(@react-router/node@8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(typescript@7.0.2))(isbot@5.2.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react-router-devtools:
specifier: ^6.2.1
version: 6.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(csstype@3.2.3)(react-dom@19.2.8(react@19.2.8))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(solid-js@1.9.14)(supports-color@10.2.2)
@@ -248,6 +257,9 @@ importers:
packages/agents:
dependencies:
'@agentos-software/git':
specifier: 0.3.3
version: 0.3.3
'@code/env':
specifier: workspace:*
version: link:../env
@@ -4473,6 +4485,9 @@ packages:
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
'@ts-morph/common@0.11.1':
resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==}
'@ts-morph/common@0.27.0':
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
@@ -4844,6 +4859,10 @@ packages:
peerDependencies:
valibot: ^1.4.0
'@vercel/config@0.5.5':
resolution: {integrity: sha512-U0QX7p08vgk8D47HI74wYyRuDJ2IYHbFQyfVdwO81Xchjp5CsV58/xhkV4EAXXj4NuLuELBwdPDa6oOdCYvEhg==}
hasBin: true
'@vercel/detect-agent@1.2.3':
resolution: {integrity: sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag==}
engines: {node: '>=14'}
@@ -4852,6 +4871,21 @@ packages:
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
engines: {node: '>= 20'}
'@vercel/react-router@1.3.1':
resolution: {integrity: sha512-cDUoEJUjeC1nf5u+LIae5e2YFUWJhKT7EpDveys7uk6zxmAHCj6swkaEf5gZPN597wcc+O0aGeK7xt7J/TRRRA==}
peerDependencies:
'@react-router/dev': '7'
'@react-router/node': '7'
isbot: '5'
react: 19.2.8
react-dom: 19.2.8
'@vercel/routing-utils@6.4.0':
resolution: {integrity: sha512-SnL/449ftNIDt1cB/oDqFmdZWywckNA6Q/S3WuzzsxBdE9DxmNn498qMKRtgx0W6EW3Mm/wVmg7aNJrfElMBTA==}
'@vercel/static-config@3.4.0':
resolution: {integrity: sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==}
'@vitest/browser-preview@4.1.9':
resolution: {integrity: sha512-a4/OrkMDb/WUnE4OOB/4FJbK3rYVO7YykqtUgcTKG4p2a0R3XcjPVu7SLRHFBs2+NIYhv5yxp1Lz3dbdGBjIow==}
peerDependencies:
@@ -5102,6 +5136,9 @@ packages:
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
ajv@8.6.3:
resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==}
anser@1.4.10:
resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==}
@@ -5239,6 +5276,9 @@ packages:
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
@@ -5376,6 +5416,9 @@ packages:
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
engines: {node: '>= 5.10.0'}
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
engines: {node: 20 || >=22}
@@ -5585,6 +5628,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
code-block-writer@10.1.1:
resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==}
code-block-writer@13.0.3:
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
@@ -5647,6 +5693,9 @@ packages:
resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
engines: {node: '>= 0.8.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concurrently@9.2.4:
resolution: {integrity: sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==}
engines: {node: '>=18'}
@@ -7325,6 +7374,9 @@ packages:
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-to-ts@1.6.4:
resolution: {integrity: sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==}
json-schema-to-ts@3.1.1:
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
engines: {node: '>=16'}
@@ -8001,6 +8053,9 @@ packages:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
@@ -8475,6 +8530,9 @@ packages:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
path-to-regexp@6.1.0:
resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==}
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -8576,6 +8634,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
pretty-cache-header@1.0.0:
resolution: {integrity: sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw==}
engines: {node: '>=12.13'}
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -9456,6 +9518,10 @@ packages:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
timestring@6.0.0:
resolution: {integrity: sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==}
engines: {node: '>=8'}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -9534,9 +9600,15 @@ packages:
resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==}
engines: {node: '>=6.10'}
ts-morph@12.0.0:
resolution: {integrity: sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==}
ts-morph@26.0.0:
resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==}
ts-toolbelt@6.15.5:
resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==}
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
@@ -14304,6 +14376,13 @@ snapshots:
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@ts-morph/common@0.11.1':
dependencies:
fast-glob: 3.3.3
minimatch: 3.1.5
mkdirp: 1.0.4
path-browserify: 1.0.1
'@ts-morph/common@0.27.0':
dependencies:
fast-glob: 3.3.3
@@ -14655,10 +14734,39 @@ snapshots:
dependencies:
valibot: 1.4.2(typescript@7.0.2)
'@vercel/config@0.5.5':
dependencies:
'@vercel/routing-utils': 6.4.0
pretty-cache-header: 1.0.0
zod: 3.25.76
'@vercel/detect-agent@1.2.3': {}
'@vercel/oidc@3.2.0': {}
'@vercel/react-router@1.3.1(@react-router/dev@8.3.0(@react-router/serve@8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2)(wrangler@4.114.0))(@react-router/node@8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(typescript@7.0.2))(isbot@5.2.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@react-router/dev': 8.3.0(@react-router/serve@8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(typescript@7.0.2)(yaml@2.9.0))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(supports-color@10.2.2)(typescript@7.0.2)(wrangler@4.114.0)
'@react-router/node': 8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(typescript@7.0.2)
'@vercel/static-config': 3.4.0
isbot: 5.2.1
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
ts-morph: 12.0.0
'@vercel/routing-utils@6.4.0':
dependencies:
path-to-regexp: 6.1.0
path-to-regexp-updated: path-to-regexp@6.3.0
optionalDependencies:
ajv: 6.15.0
'@vercel/static-config@3.4.0':
dependencies:
ajv: 8.6.3
json-schema-to-ts: 1.6.4
ts-morph: 12.0.0
'@vitest/browser-preview@4.1.9(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.9)':
dependencies:
'@testing-library/dom': 10.4.1
@@ -14901,6 +15009,13 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ajv@8.6.3:
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
anser@1.4.10: {}
ansi-colors@4.1.3: {}
@@ -15080,6 +15195,8 @@ snapshots:
bail@2.0.2: {}
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
@@ -15214,6 +15331,11 @@ snapshots:
dependencies:
big-integer: 1.6.52
brace-expansion@1.1.18:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@5.0.8:
dependencies:
balanced-match: 4.0.4
@@ -15457,6 +15579,8 @@ snapshots:
clsx@2.1.1: {}
code-block-writer@10.1.1: {}
code-block-writer@13.0.3: {}
color-convert@1.9.3:
@@ -15507,6 +15631,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
concat-map@0.0.1: {}
concurrently@9.2.4:
dependencies:
chalk: 4.1.2
@@ -17412,6 +17538,11 @@ snapshots:
json-parse-even-better-errors@2.3.1: {}
json-schema-to-ts@1.6.4:
dependencies:
'@types/json-schema': 7.0.15
ts-toolbelt: 6.15.5
json-schema-to-ts@3.1.1:
dependencies:
'@babel/runtime': 7.29.7
@@ -18384,6 +18515,10 @@ snapshots:
dependencies:
brace-expansion: 5.0.8
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.18
minimist@1.2.8: {}
minipass@7.1.3: {}
@@ -18931,6 +19066,8 @@ snapshots:
lru-cache: 11.5.2
minipass: 7.1.3
path-to-regexp@6.1.0: {}
path-to-regexp@6.3.0: {}
path-to-regexp@8.4.2: {}
@@ -19041,6 +19178,10 @@ snapshots:
prettier@3.9.6: {}
pretty-cache-header@1.0.0:
dependencies:
timestring: 6.0.0
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@@ -20306,6 +20447,8 @@ snapshots:
dependencies:
setimmediate: 1.0.5
timestring@6.0.0: {}
tiny-invariant@1.3.3: {}
tinybench@2.9.0: {}
@@ -20363,11 +20506,18 @@ snapshots:
ts-dedent@2.3.0: {}
ts-morph@12.0.0:
dependencies:
'@ts-morph/common': 0.11.1
code-block-writer: 10.1.1
ts-morph@26.0.0:
dependencies:
'@ts-morph/common': 0.27.0
code-block-writer: 13.0.3
ts-toolbelt@6.15.5: {}
tsconfck@3.1.6(typescript@7.0.2):
optionalDependencies:
typescript: 7.0.2

21
scripts/vercel-build.ts Normal file
View File

@@ -0,0 +1,21 @@
import { execFileSync } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
const appDirectory = "apps/web";
const resultPath = `${appDirectory}/.vercel/react-router-build-result.json`;
execFileSync("pnpm", ["run", "--filter", "web", "build"], {
stdio: "inherit",
});
const result = JSON.parse(await readFile(resultPath, "utf8"));
for (const bundle of Object.values(result.buildManifest.serverBundles)) {
bundle.file = `${appDirectory}/${bundle.file}`;
}
await mkdir(".vercel", { recursive: true });
await writeFile(
".vercel/react-router-build-result.json",
`${JSON.stringify(result, null, 2)}\n`
);

11
vercel.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "pnpm run vercel-build",
"installCommand": "pnpm install --frozen-lockfile",
"rewrites": [
{
"source": "/api/auth/:path*",
"destination": "https://joyous-cat-297.convex.site/api/auth/:path*"
}
]
}