Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/audit/application-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

- `docs/specs/security-local.md`
- `docs/specs/security-remote.md`
- `docs/specs/security-hosted.md`

**Output file:** `audit-application.md`

This is a code-and-specs audit of the product's own boundaries — the remote
control stack, and the local application. You need no GitHub API access and no
PAT — do not use one.

For Hosted accounts, read `docs/specs/hosted.md`, `hosted/server/`, the packed
core/auth modules in `vendor/`, and `hosted/src/`. Verify the archive hashes
against `vendor/build.json`. Distinguish tested code from pending production
configuration; do not treat local provider simulations as live OAuth acceptance.

Read, at minimum: `docs/specs/remote-security-model.md` **and its paired
`docs/specs/remote-security-model.rationale.md`**, `docs/specs/relay.md`,
`docs/specs/remote-api.md`, `docs/specs/pocket-app.md`, `SELF_HOST.md`, and then
Expand Down
170 changes: 170 additions & 0 deletions .github/workflows/hosted-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Hosted PR preview

on:
pull_request:
types: [opened, synchronize, reopened, closed]

permissions:
contents: read

# Let provisioning finish before cleanup/redeploy; canceling halfway leaks resources.
concurrency:
group: hosted-pr-preview-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
changes:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
hosted: ${{ steps.paths.outputs.hosted }}
steps:
# Build the PR merge revision, including Hosted files added to its base.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Detect changes across the full PR
id: paths
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
run: |
# GitHub caps this API at 3,000 files. Verify conservatively above the cap.
if [ "$CHANGED_FILES" -gt 3000 ]; then
echo "hosted=true" >> "$GITHUB_OUTPUT"
exit 0
fi
gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[] | .filename, (.previous_filename // empty)' > "$RUNNER_TEMP/hosted-files"
node hosted/scripts/changed.mjs "$RUNNER_TEMP/hosted-files" >> "$GITHUB_OUTPUT"

verify:
needs: changes
if: needs.changes.outputs.hosted == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# Build the PR merge revision, including Hosted files added to its base.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:hosted
- run: pnpm build:hosted
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: preview-assets
path: hosted/dist
retention-days: 3
- if: vars.HOSTED_PREVIEWS_ENABLED != 'true'
run: echo '::notice::Cloud previews are not configured yet. Follow hosted/DEPLOYMENT.md, set HOSTED_PREVIEWS_ENABLED=true, then rerun this workflow.'

deploy:
needs: verify
# Never expose deployment credentials to fork code or pull_request_target.
if: >-
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository &&
vars.HOSTED_PREVIEWS_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: hosted-preview
url: ${{ steps.deploy.outputs.url }}
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
BUILD_SHA: ${{ github.sha }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_WORKERS_SUBDOMAIN: ${{ vars.CLOUDFLARE_WORKERS_SUBDOMAIN }}
steps:
# Build the PR merge revision, including Hosted files added to its base.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: preview-assets
path: hosted/dist
- name: Check preview settings before creating resources
run: |
node --input-type=module <<'JS'
import { required } from './hosted/scripts/preview.mjs';
for (const name of ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_WORKERS_SUBDOMAIN',
'NEON_PROJECT_ID', 'NEON_PREVIEW_PARENT_BRANCH',
'CLOUDFLARE_API_TOKEN', 'NEON_API_KEY', 'PREVIEW_AUTH_SECRET'])
required(process.env, name);
JS
env:
NEON_PROJECT_ID: ${{ vars.NEON_PROJECT_ID }}
NEON_PREVIEW_PARENT_BRANCH: ${{ vars.NEON_PREVIEW_PARENT_BRANCH }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
PREVIEW_AUTH_SECRET: ${{ secrets.PREVIEW_AUTH_SECRET }}
- name: Create or reuse PR database branch
id: database
uses: neondatabase/create-branch-action@fb620d43d4c565abaf088b848a4e28e5c4ea4d9c # v6
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
parent_branch: ${{ vars.NEON_PREVIEW_PARENT_BRANCH }}
branch_name: dormouse-hosted-pr-${{ github.event.pull_request.number }}
api_key: ${{ secrets.NEON_API_KEY }}
# Use the plan default (5 minutes); Free rejects custom timeouts.
suspend_timeout: 0
- name: Apply and validate SQL migrations on this PR's database
run: pnpm --filter dormouse-hosted db:migrate --preview && pnpm --filter dormouse-hosted db:validate --preview
env:
DATABASE_URL: ${{ steps.database.outputs.db_url }}
- name: Deploy Worker and Hyperdrive
id: deploy
run: pnpm --filter dormouse-hosted preview:deploy
env:
DATABASE_URL: ${{ steps.database.outputs.db_url }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PREVIEW_AUTH_SECRET: ${{ secrets.PREVIEW_AUTH_SECRET }}
- name: Check deployed revision, database, cookies and routes
run: pnpm --filter dormouse-hosted preview:smoke "$PREVIEW_ORIGIN" "$BUILD_SHA"
env:
PREVIEW_ORIGIN: ${{ steps.deploy.outputs.url }}

cleanup:
if: >-
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository &&
vars.HOSTED_PREVIEWS_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
environment: hosted-preview
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: Remove this PR's Worker, Hyperdrive and Neon branch
run: node hosted/scripts/preview.mjs cleanup
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
NEON_PROJECT_ID: ${{ vars.NEON_PROJECT_ID }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
120 changes: 120 additions & 0 deletions .github/workflows/hosted-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Hosted production release
on:
workflow_dispatch:
inputs:
promote:
description: Deploy to hosted.dormouse.sh after verification
type: boolean
default: false
permissions:
contents: read
concurrency:
group: hosted-production
cancel-in-progress: false
jobs:
verify:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:hosted
- run: pnpm build:hosted
- name: Require accepted package provenance
run: node --input-type=module -e 'import { verifyPackages } from "./hosted/scripts/production.mjs"; await verifyPackages();'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: hosted-production-assets
path: hosted/dist
if-no-files-found: error
retention-days: 3
deploy:
needs: verify
if: inputs.promote
runs-on: ubuntu-latest
timeout-minutes: 25
environment:
name: hosted-production
url: https://hosted.dormouse.sh
outputs:
verified-at: ${{ steps.live.outputs.verified-at }}
deployment-id: ${{ steps.live.outputs.deployment-id }}
env:
BUILD_SHA: ${{ github.sha }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
HYPERDRIVE_ID: ${{ vars.HYPERDRIVE_ID }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: hosted-production-assets
path: hosted/dist
- name: Validate production identity, uncached Hyperdrive and Worker secrets
run: node hosted/scripts/production.mjs preflight
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Install backup encryption tool
run: sudo apt-get update -qq && sudo apt-get install -y age
- name: Back up and verify decryption and database restore
run: node hosted/scripts/production-backup.mjs
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BACKUP_AGE_IDENTITY: ${{ secrets.BACKUP_AGE_IDENTITY }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: hosted-backup-${{ github.run_id }}-${{ github.run_attempt }}
path: hosted/.wrangler/production-backup/*.age
if-no-files-found: error
retention-days: 30
- name: Apply and validate production migrations
run: pnpm --filter dormouse-hosted db:migrate && pnpm --filter dormouse-hosted db:validate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy verified build
run: node hosted/scripts/production.mjs deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Verify live production revision and auth boundary
id: live
run: |
node hosted/scripts/production.mjs smoke
echo "verified-at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "deployment-id=$GITHUB_RUN_ID/$GITHUB_RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
tag:
needs: deploy
runs-on: ubuntu-latest
timeout-minutes: 5
environment: hosted-release-tag
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: Record the verified Hosted deployment
run: node hosted/scripts/production-tag.mjs
env:
# GITHUB_TOKEN cannot bypass the repository's admin-only tag ruleset.
GH_TOKEN: ${{ secrets.HOSTED_TAG_TOKEN }}
BUILD_SHA: ${{ github.sha }}
DEPLOYMENT_VERIFIED_AT: ${{ needs.deploy.outputs.verified-at }}
DEPLOYMENT_ID: ${{ needs.deploy.outputs.deployment-id }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ website/public/guide/

# Environment
.env.local
hosted/.env
hosted/.dev.vars*
hosted/.wrangler/
hosted/.pgstencil/
hosted/dist-worker/

# Storybook / Chromatic
storybook-static/
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ marking a PR ready for review is what spends them.
- **`vscode-ext/`** — VS Code extension wrapping the lib in a webview (esbuild; node-pty via forked child process)
- **`website/`** — Marketing site (Vite) bundling part of the lib as an interactive demo on `FakePtyAdapter`
- **`relay/`** — Selfhost coordinating Relay for remote control (Hono): accounts + passkey auth in local JSON files (no database), WebSocket routing between Clients and Burrows, serves the built Pocket app
- **`hosted/`** — Separate Hosted account frontend and Hono Worker; packed pgstencil Better Auth, Postgres, and provider configuration.
- **`dor/`** — The `dor` CLI (stricli) staged onto the `PATH` of every Dormouse-launched terminal; talks to its host over a private control socket
- **`remote-lib-common/`** — Security primitives + remote wire contract shared by `relay`, the Burrow module in `lib`, and the Pocket app (bare ES2022 — no DOM or Node types)
- **`dor-lib-common/`** — Cross-platform external-process spawning (`spawnAndCapture`) shared by `dor` and the `lib` host. Despite the parallel names, the two `*-lib-common` packages are unrelated: `remote-lib-common` is remote security/wire, `dor-lib-common` is spawn plumbing.
Expand Down Expand Up @@ -72,6 +73,8 @@ A spec is the accurate reference for the current code: it states the invariants
- **`docs/specs/remote-security-model.md`** — Remote-control trust model: one Noise channel per ceremony, passkeys proving presence inside it, per-Burrow Client statics, the Burrow (not the Relay) authorizing the pair. Read first for anything remote.
- **`docs/specs/remote-api.md`** — What an authorized Client speaks: the shipped terminal-only **protocol-v1** and the staged remainder.
- **`docs/specs/relay.md`** — The selfhost coordinating Relay and shared Burrow-service runtime: env config, JSON-file state, WebAuthn without a library, HTTP API, relay flow, enrollment, running it end to end.
- **`docs/specs/hosted.md`** — Hosted accounts: application boundary, login/linking policy, local development, and staged paid services.
- **`docs/specs/security-hosted.md`** — Hosted account origin, identity, and deployment security checks.
- **`SELF_HOST.md`** (repo root) — Self-host deployment: the assistant-run install runbook plus the Installer contract that `docs/specs/security-remote.md`'s `FAIL IF` lines and `scripts/deploy-lint.mjs` audit.
- **`docs/specs/pocket-app.md`** — Pocket: the remote session is a `PlatformAdapter` (`RemotePtyAdapter`), so Pocket is auth screens plus the mobile composition; owns the same-origin deployment rule.
- **`docs/specs/deploy.md`** — Release process: artifact matrix, release checklist, two-stage sign-and-release pipeline, updater manifest, changelog flow.
Expand Down
4 changes: 4 additions & 0 deletions docs/specs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ Source of truth: `create_release` in `scripts/sign-and-deploy.sh`; `website/scri

`docs/specs/security-ci.md` -> "Desktop Releases" owns the argv-exposure rules for the three prompted secrets.

## Hosted account releases

See `docs/specs/hosted.md` -> "Production releases" for the Hosted pipeline and `hosted/DEPLOYMENT.md` for provisioning and operator commands.

## Future

**Analytics-backed download URLs.** The `/latest/download/` hotlinks could move to `dormouse.sh/download/...` behind Cloudflare R2. Changing website links and manifest bundle URLs needs no app update while the manifest endpoint remains stable.
Loading
Loading