{"id":47969692,"url":"https://github.com/marcusrbrown/infra","last_synced_at":"2026-06-14T22:01:23.867Z","repository":{"id":348865724,"uuid":"1200110668","full_name":"marcusrbrown/infra","owner":"marcusrbrown","description":"Personal infrastructure management","archived":false,"fork":false,"pushed_at":"2026-06-08T01:39:04.000Z","size":1734,"stargazers_count":1,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T03:20:51.818Z","etag":null,"topics":["bun","deploy","github-actions","infra","keeweb"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marcusrbrown.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"marcusrbrown"}},"created_at":"2026-04-03T03:40:15.000Z","updated_at":"2026-06-08T01:37:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/marcusrbrown/infra","commit_stats":null,"previous_names":["marcusrbrown/infra"],"tags_count":46,"template":false,"template_full_name":null,"purl":"pkg:github/marcusrbrown/infra","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcusrbrown%2Finfra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcusrbrown%2Finfra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcusrbrown%2Finfra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcusrbrown%2Finfra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marcusrbrown","download_url":"https://codeload.github.com/marcusrbrown/infra/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcusrbrown%2Finfra/sbom","scorecard":{"id":1245536,"data":{"date":"2026-04-03T14:28:33Z","repo":{"name":"github.com/marcusrbrown/infra","commit":"de4edc28736182bc33af781709e14b3943e99816"},"scorecard":{"version":"v5.3.0","commit":"c22063e786c11f9dd714d777a687ff7c4599b600"},"score":5.4,"checks":[{"name":"Code-Review","score":1,"reason":"Found 1/10 approved changesets -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#binary-artifacts"}},{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: RenovateBot: .github/renovate.json5:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dependency-update-tool"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info:  10 out of  10 GitHub-owned GitHubAction dependencies pinned","Info:  13 out of  13 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: jobLevel 'contents' permission set to 'write': .github/workflows/release.yaml:26","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yaml:10","Info: topLevel 'contents' permission set to 'read': .github/workflows/deploy.yaml:4","Info: topLevel 'contents' permission set to 'read': .github/workflows/fro-bot.yaml:22","Info: topLevel 'contents' permission set to 'read': .github/workflows/release.yaml:15","Info: topLevel 'contents' permission set to 'read': .github/workflows/renovate-changesets.yaml:8","Info: topLevel 'contents' permission set to 'read': .github/workflows/renovate.yaml:30","Info: topLevel permissions set to 'read-all': .github/workflows/scorecard.yaml:12","Warn: no topLevel permission defined: .github/workflows/update-repo-settings.yaml:1"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#packaging"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#license"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#signed-releases"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 5 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#sast"}},{"name":"CI-Tests","score":10,"reason":"5 out of 5 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#ci-tests"}},{"name":"Contributors","score":10,"reason":"project has 5 contributing companies or organizations","details":["Info: found contributions from: UnrealPhx, bfra-me, ethereumclassic, ps2dev, pspdev"],"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#contributors"}}]},"last_synced_at":"2026-04-03T16:39:44.292Z","repository_id":348865724,"created_at":"2026-04-03T16:39:44.292Z","updated_at":"2026-04-03T16:39:44.292Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34339195,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","deploy","github-actions","infra","keeweb"],"created_at":"2026-04-04T10:42:24.508Z","updated_at":"2026-06-14T22:01:23.845Z","avatar_url":"https://github.com/marcusrbrown.png","language":"TypeScript","funding_links":["https://github.com/sponsors/marcusrbrown"],"categories":[],"sub_categories":[],"readme":"# @marcusrbrown/infra\n\nPersonal infrastructure management — deploy automation, operational CLI, and tooling.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@marcusrbrown/infra\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@marcusrbrown/infra?style=flat-square\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/marcusrbrown/infra/actions/workflows/ci.yaml\"\u003e\u003cimg src=\"https://github.com/marcusrbrown/infra/actions/workflows/ci.yaml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/marcusrbrown/infra/actions/workflows/codeql.yaml\"\u003e\u003cimg src=\"https://github.com/marcusrbrown/infra/actions/workflows/codeql.yaml/badge.svg\" alt=\"CodeQL\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://scorecard.dev/viewer/?uri=github.com/marcusrbrown/infra\"\u003e\u003cimg src=\"https://api.scorecard.dev/projects/github.com/marcusrbrown/infra/badge?style=flat-square\" alt=\"OpenSSF Scorecard\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/marcusrbrown/infra/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/marcusrbrown/infra?style=flat-square\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## Overview\n\nBun workspace monorepo for managing personal infrastructure. Hosts KeeWeb deploy automation, the CLIProxyAPI proxy that routes Fro Bot agents to Claude via the Claude Code OAuth subscription, the Fro Bot gateway Discord client and workspace runner, a WireGuard VPN egress box on AWS Lightsail, and a CLI for operational health checks, deploy triggers, and MCP tool exposure.\n\n| Package | Description |\n| --- | --- |\n| `apps/keeweb` | KeeWeb v1.18.7 static site deploy automation (`kw.igg.ms`) |\n| `apps/cliproxy` | CLIProxyAPI Docker Compose stack behind Caddy (`cliproxy.fro.bot`) |\n| `apps/gateway` | Fro Bot gateway Docker Compose stack (`gateway.fro.bot`) |\n| `apps/umami` | Self-hosted Umami analytics Docker Compose stack (`metrics.fro.bot`) |\n| `apps/vpn` | WireGuard egress box on AWS Lightsail `eu-west-1` — native `wg-quick@wg0`, no Docker |\n| `packages/cli` | [`@marcusrbrown/infra`](https://www.npmjs.com/package/@marcusrbrown/infra) CLI — health checks, deploy triggers, onboarding wizard, MCP bridge |\n\n## Prerequisites\n\n- [Bun](https://bun.sh) v1.0+\n- [GitHub CLI](https://cli.github.com) (`gh`) — required for the remote `keeweb`/`cliproxy` deploy triggers and status commands\n\n## Quick Start\n\n```bash\nbun install\nbun run lint\nbunx tsc --noEmit\nbun test --recursive\n```\n\n## Apps\n\nEach app is a self-contained deploy unit. See its README for build, deploy, provisioning, and configuration detail; see its `AGENTS.md` for operational runbooks.\n\n- **[KeeWeb](apps/keeweb/README.md)** (`apps/keeweb`) — self-hosted [KeeWeb](https://keeweb.info) password manager at [kw.igg.ms](https://kw.igg.ms); static site deployed over SSH/rsync to a Mail-in-a-Box server.\n- **[CLIProxyAPI](apps/cliproxy/README.md)** (`apps/cliproxy`) — [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) Claude proxy at [cliproxy.fro.bot](https://cliproxy.fro.bot); Docker Compose + Caddy on DigitalOcean, issuing per-repo API keys backed by one Claude subscription.\n- **[Gateway](apps/gateway/README.md)** (`apps/gateway`) — Fro Bot Discord client and workspace runner at [gateway.fro.bot](https://gateway.fro.bot); a 3-service Docker Compose stack on DigitalOcean, pinned to `fro-bot/agent` via `apps/gateway/upstream.json`.\n- **[Umami](apps/umami/README.md)** (`apps/umami`) — self-hosted [Umami](https://umami.is) privacy-respecting analytics at [metrics.fro.bot](https://metrics.fro.bot); Docker Compose (Umami + Postgres + Caddy) on DigitalOcean.\n- **[VPN](apps/vpn/README.md)** (`apps/vpn`) — WireGuard egress box on AWS Lightsail (`eu-west-1`, Ireland); native `wg-quick@wg0` + systemd, no Docker; provisioned via `@aws-sdk/client-lightsail`, deployed over SSH.\n\n## CLI\n\nThe [`@marcusrbrown/infra`](https://www.npmjs.com/package/@marcusrbrown/infra) CLI (`packages/cli`) is the unified operator surface — one command group per app, a unified `status` dashboard, and an MCP bridge. See [`packages/cli/README.md`](packages/cli/README.md) for the full command reference.\n\n```bash\nbunx @marcusrbrown/infra --help\nbunx @marcusrbrown/infra status          # all deployments, human-readable\nbunx @marcusrbrown/infra status --json   # machine-readable\nbunx @marcusrbrown/infra mcp             # stdio MCP server for coding agents\n```\n\n## CI/CD\n\n### Workflows\n\n| Workflow | Trigger | Purpose |\n| --- | --- | --- |\n| **CI** | PRs to `main` | Lint, type check, and test |\n| **Deploy KeeWeb** | Push to `main`, `workflow_dispatch` | Build and deploy KeeWeb (path-filtered) |\n| **Deploy CLIProxy** | Push to `main`, `workflow_dispatch` | Deploy CLIProxyAPI (path-filtered) |\n| **Deploy Gateway** | Push to `main`, `workflow_dispatch` | Deploy gateway stack (path-filtered) |\n| **Deploy Umami** | Push to `main`, `workflow_dispatch` | Deploy Umami analytics stack (path-filtered) |\n| **Deploy VPN** | Push to `main`, `workflow_dispatch` | Deploy WireGuard VPN box (path-filtered) |\n| **Deploy** | Push to `main`, `workflow_dispatch` | Router that path-filters changes and dispatches the per-app deploy workflows |\n| **Release** | Push to `main` | Version and publish `@marcusrbrown/infra` via Changesets |\n| **Renovate** | Schedule, issue/PR edits, post-deploy | Automated dependency updates |\n| **Renovate Changesets** | Renovate PRs | Auto-create changeset files for dependency updates |\n| **Fro Bot** | PRs, @mentions, daily schedule, `workflow_dispatch` | AI code review and autohealing |\n| **Copilot Setup Steps** | `workflow_dispatch`, changes to workflow file | Prepare environment for Copilot coding agent |\n| **Scorecard** | Weekly, push to `main` | OpenSSF security analysis |\n| **Update Repo Settings** | Daily, push to `main` | Sync repo settings from `.github/settings.yml` |\n\n### Deploy Pipeline\n\nThe `Deploy` router uses `dorny/paths-filter` (`predicate-quantifier: every`) to deploy only when an app's files change (docs, tests, fixtures, and snapshots are excluded from the filter). Each per-app deploy runs in its own GitHub Environment and requires approval.\n\n- **Deploy KeeWeb** runs in the `keeweb` environment.\n- **Deploy CLIProxy** runs in the `cliproxy` environment.\n- **Deploy Gateway** runs in the `gateway` environment.\n- **Deploy Umami** runs in the `umami` environment.\n- **Deploy VPN** runs in the `vpn` environment.\n\nManual deploys are available either per-app (`workflow_dispatch` on each dedicated workflow) or together via the umbrella `Deploy` workflow.\n\n### Required Secrets\n\n**`keeweb` environment:**\n\n| Secret               | Description                                           |\n| -------------------- | ----------------------------------------------------- |\n| `DEPLOY_SSH_KEY`     | Ed25519 private key for `deploy-kw@box.heatvision.co` |\n| `DROPBOX_APP_SECRET` | Dropbox app client credential for KeeWeb config       |\n\n**`cliproxy` environment:**\n\n| Secret                    | Description                                                  |\n| ------------------------- | ------------------------------------------------------------ |\n| `CLIPROXY_SSH_KEY`        | Ed25519 private key for the `cliproxy.fro.bot` DO droplet    |\n| `CLIPROXY_MANAGEMENT_KEY` | Management API bearer token for runtime config / key updates |\n| `CLIPROXY_DOMAIN`         | FQDN of the CLIProxyAPI instance                             |\n\n**`gateway` environment:**\n\n| Secret | Required | Description |\n| --- | --- | --- |\n| `GATEWAY_SSH_KEY` | ✓ | Ed25519 private key for the `gateway.fro.bot` droplet |\n| `DISCORD_TOKEN` | ✓ | Discord bot token |\n| `DISCORD_APPLICATION_ID` | ✓ | Discord application ID |\n| `DISCORD_GUILD_ID` | ✓ | Discord guild (server) ID |\n| `AWS_ACCESS_KEY_ID` | ✓ | S3/R2 access key |\n| `AWS_SECRET_ACCESS_KEY` | ✓ | S3/R2 secret key |\n| `S3_BUCKET` | ✓ | Bucket name |\n| `S3_REGION` | ✓ | Bucket region |\n| `GATEWAY_HOST` | ✓ | FQDN of the droplet |\n| `GH_APP_ID` | ✓ | GitHub App ID for `/fro-bot add-project` repo access |\n| `GH_APP_PRIVATE_KEY` | ✓ | GitHub App private key PEM |\n| `WORKSPACE_OPENCODE_TOKEN` | ✓ | Internal bearer token between gateway and workspace OpenCode proxy |\n| `WORKSPACE_OPENCODE_AUTH` | ✓ | OpenCode provider `auth.json` for the workspace |\n| `WORKSPACE_OPENCODE_MODEL` | ✓ | OpenCode model ID for the mention loop |\n| `WORKSPACE_OPENCODE_CONFIG` | ✓ | OpenCode provider/baseURL config JSON |\n| `GATEWAY_TRIGGER_ROLE_ID` | ✓ | Discord role ID allowed to trigger the `@fro-bot` mention loop |\n| `S3_ENDPOINT` |  | Custom endpoint URL (R2, MinIO, etc.) |\n| `OBJECT_STORE_HOSTS` |  | Comma-separated hostnames allowed through mitmproxy egress filter |\n| `GATEWAY_WEBHOOK_SECRET` |  | HMAC key for the announce webhook (set with `GATEWAY_PRESENCE_CHANNEL_ID`) |\n| `GATEWAY_PRESENCE_CHANNEL_ID` |  | Discord channel ID for presence embeds (set with `GATEWAY_WEBHOOK_SECRET`) |\n\nSee [`apps/gateway/README.md`](apps/gateway/README.md) for the complete contract including CI-injected image digests and OpenCode supervisor tuning.\n\n**`umami` environment:**\n\n| Secret                 | Required | Description                                                                   |\n| ---------------------- | -------- | ----------------------------------------------------------------------------- |\n| `UMAMI_SSH_KEY`        | ✓        | Ed25519 private key for the `metrics.fro.bot` droplet                         |\n| `UMAMI_DOMAIN`         | ✓        | FQDN of the Umami instance                                                    |\n| `UMAMI_APP_SECRET`     | ✓        | Umami app secret (session/JWT signing)                                        |\n| `UMAMI_DB_PASSWORD`    | ✓        | Postgres password (volume-coupled — rotate only via the `ALTER USER` runbook) |\n| `UMAMI_ADMIN_PASSWORD` | ✓        | Admin password set during deploy rotation                                     |\n\nSee [`apps/umami/README.md`](apps/umami/README.md) and [`apps/umami/AGENTS.md`](apps/umami/AGENTS.md) for the DB-password rotation runbook.\n\n**`vpn` environment:**\n\n| Secret        | Required | Description                                                                      |\n| ------------- | -------- | -------------------------------------------------------------------------------- |\n| `VPN_SSH_KEY` | ✓        | Ed25519 private key for the VPN box (`wg-egress` keypair)                        |\n| `VPN_HOST`    | ✓        | Static IP of the Lightsail instance (printed by provisioning)                    |\n| `VPN_PEERS`   | —        | Peer roster JSON. Auto-synced by `vpn client add/remove`. Empty roster is valid. |\n\nAWS provisioning credentials (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) are operator-local only — not in the `vpn` Environment and not used by deploy or status. See [`apps/vpn/README.md`](apps/vpn/README.md) and [`apps/vpn/AGENTS.md`](apps/vpn/AGENTS.md).\n\n**Repository secrets:**\n\n| Secret                      | Description                                                                |\n| --------------------------- | -------------------------------------------------------------------------- |\n| `APPLICATION_ID`            | GitHub App ID for Renovate and repo settings sync                          |\n| `APPLICATION_PRIVATE_KEY`   | GitHub App private key                                                     |\n| `DIGITALOCEAN_ACCESS_TOKEN` | DigitalOcean API token (used by cliproxy, gateway, and umami provisioning) |\n| `FRO_BOT_PAT`               | PAT for the `fro-bot` user (agent identity for `@fro-bot` mentions)        |\n| `NPM_TOKEN`                 | npm publish token for `@marcusrbrown/infra` package                        |\n| `OPENCODE_AUTH_JSON`        | LLM provider credentials JSON injected into Fro Bot runs                   |\n| `OPENCODE_CONFIG`           | OpenCode provider config JSON (e.g. Anthropic `baseURL` override)          |\n\n**Repository variables:**\n\n| Variable        | Description                                                   |\n| --------------- | ------------------------------------------------------------- |\n| `FRO_BOT_MODEL` | LLM model ID for the Fro Bot agent (e.g. `claude-sonnet-4-6`) |\n\n### Server Setup\n\nThe KeeWeb deploy target uses a dedicated `deploy-kw` user with scoped sudo for the nginx activation script. To provision or re-provision the user:\n\n```bash\nbun run apps/keeweb/server/setup-deploy-user.ts\n```\n\nHost keys for `box.heatvision.co`, `cliproxy.fro.bot`, `gateway.fro.bot`, and the VPN static IP are pinned in `.github/known_hosts` — no runtime `ssh-keyscan`.\n\n## Repository Structure\n\nFor the directory layout and where to put new code, see [`STRUCTURE.md`](STRUCTURE.md). For system shape, data flow, and invariants, see [`ARCHITECTURE.md`](ARCHITECTURE.md).\n\n## Testing\n\n```bash\nbun test --recursive  # Run all tests from repo root\nbun test              # Run tests in current package\n```\n\nTests are colocated alongside source files (`*.test.ts`). Fixtures live in `__fixtures__/`, snapshots in `__snapshots__/`. Tests mock at boundaries (`fetch`, `Bun.spawn`) and use `NO_COLOR=1` for deterministic subprocess output. CI runs tests as a parallel job alongside lint and type-check.\n\n## Development\n\n```bash\nbun run lint          # ESLint\nbun run fix           # ESLint --fix (includes Prettier)\nbunx tsc --noEmit     # Type check\n```\n\nPre-commit hook runs `lint-staged` → `eslint --fix` on staged files via `simple-git-hooks`.\n\n### Tooling\n\n| Tool       | Config                                                                                         |\n| ---------- | ---------------------------------------------------------------------------------------------- |\n| ESLint     | `eslint.config.ts` via `@bfra.me/eslint-config`                                                |\n| Prettier   | `@bfra.me/prettier-config/120-proof`                                                           |\n| TypeScript | `tsconfig.json` via `@bfra.me/tsconfig`                                                        |\n| Git hooks  | `simple-git-hooks` + `lint-staged`                                                             |\n| CLI        | [goke](https://github.com/remorses/goke) + Zod Standard Schemas                                |\n| Prompts    | [`@clack/prompts`](https://github.com/bombshell-dev/clack) — scoped to `cliproxy setup` wizard |\n| Changesets | `@changesets/cli` for versioning                                                               |\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcusrbrown%2Finfra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcusrbrown%2Finfra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcusrbrown%2Finfra/lists"}