diff --git a/.agents/skills/deploy-fresh-instance/SKILL.md b/.agents/skills/deploy-fresh-instance/SKILL.md index a1e0743..42c71fa 100644 --- a/.agents/skills/deploy-fresh-instance/SKILL.md +++ b/.agents/skills/deploy-fresh-instance/SKILL.md @@ -1,146 +1,80 @@ --- name: deploy-fresh-instance -description: Deploy a fresh a-drive instance to Cloudflare Workers — provision Postgres (PlanetScale + Hyperdrive), R2, and KV plus Workers AI, set the PASSCODE secret, attach the two custom domains, and cut the first release. Use for the first-ever deploy to a new Cloudflare account or a new operator domain (not for routine redeploys, which are just `bun release`). +description: Bootstrap a fresh hosted a-drive deployment with Cloudflare Workers, Postgres through Hyperdrive, R2, KV, Queues, WorkOS, and optional paid billing. Use for a new account or target domain. Routine releases use bun release and the maintained release runbook. --- -# Deploy a fresh a-drive instance to Cloudflare - -This is the proven first-deploy sequence, executed successfully for -`davis7.space` (Aug 2026). It complements `docs/release.md` (the canonical -first-time-setup + release + rollback doc) — read that alongside this; the -steps here add the exact commands, the paste-the-id-yourself reality, and -the post-deploy gotchas that actually bit us. It does not repeat the -rollback or migration-rule sections — see `docs/release.md` for those. - -Reference example throughout: dashboard `drive.davis7.space`, content -`files.davis7.space`, resource name stem `adrive-production`. Substitute -the operator's own domain, hostnames, and names. - -## 1. Prerequisites - -- `wrangler whoami` succeeds and points at the intended Cloudflare account. -- The operator's domain is already added as a **Cloudflare zone** (the - custom-domain routes attach automatically on deploy only if the zone - exists). Remove conflicting CNAME records for the two application - hostnames; the deploy creates their DNS records, but not the zone. -- Node 26 + Bun 1.4 installed; run `bun install` once at the repo root. -- Decide the **two hostnames** now — a dashboard origin and a content - origin. They must be **distinct hosts** (e.g. `drive.` and `files.`); - the app rejects cross-origin serving and several checks depend on the - split. - -## 2. Provisioning (run from `apps/web`) - -Wrangler runs **non-interactively** here: each create prints an id and -exits. It does **not** edit `wrangler.jsonc` for you — you paste each id -into `env.production` by hand. - -1. Create the PlanetScale Postgres database and a Hyperdrive config for it - (`wrangler hyperdrive create adrive-production --connection-string=... --caching-disabled`) - → paste the printed id into `env.production.hyperdrive[0].id`. Keep the - connection string; `bun release` needs it as `DATABASE_URL`. -2. `wrangler r2 bucket create adrive-production` - (no id to paste — the bucket is bound by name.) -3. `wrangler kv namespace create AUTH_GUARD --env production` - → paste the printed id into `env.production.kv_namespaces[0].id`. -4. Semantic search needs no extra provisioning. Embeddings come from - the Workers AI `ai` binding already declared in `wrangler.jsonc` - `env.production`, and the vectors are stored in Postgres - (`file_chunks.embedding`, pgvector), which the `HYPERDRIVE` binding - already reaches. - - Note: `env.production` sets `SEMANTIC_SEARCH=required` (not `auto`), so a - missing `AI` binding fails the deploy **loudly** instead of silently - degrading to keyword search. That is intended. - -5. In the Cloudflare dashboard, open **Images → Transformations**, select - the zone that owns `CONTENT_DOMAIN` (`davis7.space` for - `files.davis7.space`), and enable transformations. Dashboard thumbnails - require this zone-level setting. - -## 3. Set the passcode secret (from `apps/web`) - -``` -wrangler secret put PASSCODE --env production -``` - -12+ characters, typed by the operator at the prompt — never put the -passcode in chat, a command argument, or a log. - -**Gotcha (expected):** on a first-ever setup the Worker shell doesn't -exist yet, so wrangler prompts _"There doesn't seem to be a Worker called -adrive-production. Do you want to create it?"_ — answer **yes**. This is -normal; it stages an empty Worker so the secret has somewhere to live -before the first deploy. - -## 4. Commit the provisioned ids - -The Hyperdrive `id` and KV `id` are **configuration, not secrets** — -commit the edited `wrangler.jsonc`. Two `bun release` preflight gates -depend on it: - -- **clean-tree gate** — the release refuses a dirty working tree, so the - pasted ids must be committed. -- **no-placeholder gate** — the release greps for `replace-with--*` - placeholders (e.g. `replace-with-production-...`) and refuses while any - remain. Pasting the real ids clears this. - -## 5. Deploy - -From the **repo root**: - -``` -bun release -``` - -`scripts/release.sh` runs the full gate → format check → type/lint → -tests → audit → build → deploy dry-run → Postgres migrations → deploy → append -the commit to `.release-history`. The two custom domains attach from the -`routes` in `env.production` automatically, provided the zone from step 1 -exists. - -## 6. Post-deploy verification gotchas (both hit on the davis7.space run) - -- **Transient 500s right after deploy.** The very first requests can 500 - for a few seconds while the Hyperdrive pool warms against the first live - request. Recheck once things settle — it clears on its own. -- **The second (content) custom domain lags the first.** The content - origin's DNS record and edge certificate propagate minutes behind the - dashboard origin. Worse, a **local DNS negative cache** can pin it as - broken _only on the operator's machine_: if you resolved the host before - the record existed, an NXDOMAIN is cached locally while public resolvers - already serve the new record. Fix by flushing local DNS — - `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder` on macOS - — or wait out the negative TTL. To confirm the edge is actually up while - bypassing local DNS, resolve against Cloudflare directly: - `curl --resolve :443: https:///`. - -Expected live checks once both origins are up: - -- dashboard root → **200** -- unauth `GET /api/files` → **401** -- `GET https://./api/files` → **421** (dashboard API rejected on a content host) -- `GET /f/` → **421** (content rejected on dashboard origin) -- `GET https:///f/` → **421** (content needs an org host) -- `GET https://no-such-org./f/` → **404** -- missing file → **404** -- a known non-empty public file without a `Range` header → **200** with no - `Content-Range` -- the same file with `Range: bytes=0-99` → **206** with a valid `Content-Range` - -## 7. Backfill note (semantic search) - -Any files uploaded while the `AI` binding was absent sit in -`index_state = 'disabled'` and are backfilled by the maintenance cron at -about **5 files per 5 minutes**. The settings page's indexed-chunk count -shows progress. On a truly fresh instance there's usually nothing to -backfill; this matters when seeding a pre-existing corpus. - -## 8. Then - -- Run the **verify-deployment** skill (`.agents/skills/verify-deployment`) - as the acceptance gate — the release is not complete until it reports - PASS against the live deployment. -- Set up **independent backups** on a machine outside Cloudflare and - complete the restore drill (`docs/backup-restore.md`) before treating - the drive as the sole copy of anything. +# Deploy a fresh hosted a-drive instance + +Follow [release setup](../../../docs/release.md) for provisioning, exact +commands, migration compatibility, and rollback. This skill identifies the +bootstrap decisions and acceptance gates; historical single-tenant deploys +are not proof of the hosted architecture. + +## Establish the target + +- Confirm the intended Cloudflare account, database provider, dashboard + origin, content domain, resource names, and existing authorization. Keep + credentials out of command output, source, and reports. +- Use a separate, empty hosted Postgres target. Migration 0004 rejects + populated single-tenant data; do not bypass it with resets. Preserve the + original drive and backups. An existing-data import/cutover is separate + work described in the release runbook, not implicit in fresh bootstrap. +- Content lives under `https://.`; the dashboard must + be outside that domain. Configure the wildcard Worker route, proxied DNS, + and a certificate covering actual tenant hosts. A certificate for the + bare content host does not cover its subdomains. +- Inspect both `apps/web/wrangler.jsonc` and `apps/site/wrangler.jsonc`: + `bun release` deploys the app and landing-site Workers. Replace the example + routes and resource IDs with this target's values before releasing. + +## Provision and configure + +Use the release runbook's sequence and current CLI/provider interfaces: + +1. Provision Postgres with required extensions and run hosted migrations + using schema-admin credentials. Create a separate non-owner runtime + login with `adrive_app` privileges, no superuser/BYPASSRLS, and no schema + ownership. Hyperdrive uses that login with query caching disabled; + `DATABASE_URL` for migrations uses the schema administrator. +2. Provision the target R2 bucket and `AUTH_GUARD` KV namespace. Configure + `HYPERDRIVE`, `BUCKET`, `AUTH_GUARD`, `BROWSER`, rate limits, and scheduled + maintenance in the target environment. Environment bindings do not + inherit from local defaults. Semantic search stores vectors in Postgres; + supply the Workers AI binding when `SEMANTIC_SEARCH=required`. +3. Create the main, dead-letter, and parked queues exactly as described in + [Queues](../../../docs/release.md#queues). Bind the producer and both + consumers; the parked queue has no automatic consumer and retains + messages for 14 days. Configure monitoring and recovery before launch. +4. Configure real WorkOS authentication, the dashboard callback, and signed + webhook endpoint. Set `MAINTENANCE_SECRET`, `WORKOS_API_KEY`, + `WORKOS_CLIENT_ID`, `WORKOS_COOKIE_PASSWORD`, `WORKOS_WEBHOOK_SECRET`, and + intended `ADMIN_USER_IDS` via the provider's secret/configuration path. + Use [the launch checklist](../../../docs/launch-checklist.md) for current + requirements. Development fake auth/provider keys must remain disabled. +5. For paid launch, publish the plans and set `AUTUMN_SECRET_KEY` and + `AUTUMN_WEBHOOK_SECRET`; validate the integration in its sandbox using + [billing](../../../docs/billing.md). Configure URL scanning and cache + purge using [abuse operations](../../../docs/abuse.md), or record the + limitations of disabled optional providers. Enable zone image + transformations when required for content thumbnails. + +## Release and acceptance + +- Commit the target's nonsecret resource IDs/routes and confirm the release + checkout is clean. Preserve unrelated work; do not discard it to satisfy + the clean-tree gate. +- Run `bun release` from the repository root with `DATABASE_URL` pointing at + the intended hosted target. Read `scripts/release.sh` before execution: + it validates, builds, dry-runs, migrates Postgres, deploys both Workers, + and records the app revision. A partial deployment requires the runbook's + rollback/recovery decision; do not repeatedly deploy without diagnosis. +- Confirm live DNS, TLS, deployed resources, and revision. Investigate + errors from actual logs; do not assume 500 responses are normal warm-up. +- Run [verify-deployment](../verify-deployment/SKILL.md) with real WorkOS + access and disposable tenant data. Live provider and tenant-isolation + checks must not be replaced by fake/local test results. +- Complete [independent backups and an isolated restore drill](../../../docs/backup-restore.md), + [observability](../../../docs/observability.md), and the remaining launch + checklist. Return the reachable deployment URLs, deployed revision, + verification evidence, and any outstanding launch gates. A successful + deploy command alone does not complete the hosted launch. diff --git a/.agents/skills/verify-deployment/SKILL.md b/.agents/skills/verify-deployment/SKILL.md index 69f682f..0d343f5 100644 --- a/.agents/skills/verify-deployment/SKILL.md +++ b/.agents/skills/verify-deployment/SKILL.md @@ -1,238 +1,116 @@ --- name: verify-deployment -description: Verify a live a-drive deployment end to end through its real browser, HTTP, CLI, D1, and R2 boundaries. Use after deploying, after storage/auth/upload/routing changes, after migrations, and before trusting a-drive following Cloudflare configuration changes. Creates only uniquely-prefixed disposable files, purges them afterward, and produces a pass/fail report. +description: Verify a hosted a-drive deployment through real WorkOS browser sessions, tenant-scoped HTTP and CLI access, Postgres, R2, and background jobs. Use after deployment or changes to authentication, storage, routing, migrations, or provider configuration. Report evidence and clean up disposable verification data. --- # Verify an a-drive deployment -You are verifying a real, live deployment. Use whatever browser, -HTTP, and shell tooling you have; nothing here assumes a specific -framework. Work through every section, record each check as PASS, -FAIL, or SKIPPED (with reason), and finish with the report. - -Checks marked **[M]** are mandatory: the deployment cannot PASS if any -of them failed **or was skipped**. Unmarked checks are optional; they -may be SKIPPED with a documented reason. - -## Inputs (ask if not provided) - -- Dashboard origin (e.g. `https://drive.davis7.space`) -- Content origin (e.g. `https://files.davis7.space`) -- CLI path (repo checkout: `bun adrive`, needs Node 26+) -- Access method: the passcode (full run), or an existing read-write API - key (reduced run — see below) -- Whether destructive checks may run (default: **yes** — they touch only - files this run creates) - -**Access-mode branching:** passcode access enables the full run. With -only an API key, record as SKIPPED (reason: no passcode) every check -that needs a browser session or key management: browser sign-in, -wrong-passcode, dashboard uploads and previews, disposable key creation -(read-write, read-only, and short-expiry), the read-only-scope 403 -checks, the key-listing check, cookie-attribute checks, and -cookie-origin forgery checks. Because several of those are mandatory, a -key-only run can conclude at best INCONCLUSIVE — never PASS. - -## Safety boundaries (non-negotiable) - -- Generate a collision-resistant run prefix first: - `verify--`. Before - creating anything, search for the prefix and confirm nothing matches. - Every file, site, tag, and API key you create must carry it. -- Never modify, trash, rename, or purge anything without the prefix. -- Never print passcodes, API keys, session cookies, or signed URLs in - the report. Refer to them as ``; show only HTTP statuses, - header names/values that aren't credentials, and file ids. -- Purge only objects created during this run, and always attempt cleanup - even after failures. - -## 1. Preflight - -- **[M]** Both configured origins use `https://` and are different hosts. -- **[M]** Both origins resolve and respond (any status). -- Request `http://` on both hostnames: assert a redirect to HTTPS or a - refusal, and that no authenticated data or file content is served - over plain HTTP. -- Record the deployed commit if available (`.release-history` in the - repo, or `wrangler deployments list --env production` when wrangler - is authenticated) plus the current UTC time. -- **[M]** If wrangler is available: confirm the production env lists DB - (D1), BUCKET (R2), AUTH_GUARD (KV), HYPERDRIVE (Postgres), and AI - (Workers AI) bindings (`wrangler deploy --dry-run --env production` output). Without - wrangler this mandatory check is SKIPPED and the run cannot PASS. - -## 2. Authentication - -Run every negative check in a fresh browser context (or cookie-less -curl) so no prior session can mask a failure. - -- **[M]** Dashboard sign-in with the passcode succeeds in a real - browser. -- **[M]** In a fresh context: a wrong passcode fails with an error and - sets no session cookie. -- **[M]** In a fresh context: unauthenticated `GET /api/files` - returns 401. -- **[M]** Create a disposable **read-write** API key named with the run - prefix (dashboard settings), or complete CLI device authorization - (`bun adrive login `). -- **[M]** Create a disposable **read-only** key; verify `GET /api/files` - succeeds while each of these mutations returns 403: an upload, a - rename, a tag creation, a trash, and a version upload. -- **[M]** With the read-only key, `GET /api/auth/keys` must return 403 - (key inventory requires write scope). -- Create a key with a short expiry (e.g. two minutes), wait for it to - lapse, and confirm requests with it return 401. -- If the run may rotate the passcode (scratch deployment only — ask - first): create a session and a pending device authorization, change - the PASSCODE secret, wait for the maintenance cron, and confirm both - are revoked. On the production deployment SKIP with reason. - -## 3. Upload coverage - -Create small local test files: text (.txt), image (.png), PDF (.pdf), -and binary (a few hundred random bytes, .bin). Record each one's -SHA-256 before upload. - -- **[M]** Upload the text and image through the dashboard. -- **[M]** Upload the PDF and binary through the CLI (`bun adrive put`). -- **[M]** After each upload, verify via `GET /api/files/`: display - name, size, content type match the source. -- **[M]** Download each file and compare checksums with the source. -- Negative cases (verify the documented rejection status and that no - partial file, version, or R2 object is left behind): - - An upload whose declared size exceeds the per-file limit. - - A chunked upload with no Content-Length whose streamed bytes exceed - the per-file limit (the streaming reader must cut it off). - - A burst of uploads beyond the configured upload rate (429). - - If a scratch quota configuration is available, an upload beyond - `MAX_TOTAL_BYTES` (413); otherwise SKIP with reason. - -## 4. File lifecycle (on prefixed files only) - -- **[M]** List files via dashboard and CLI; all uploads appear. -- Create enough prefixed files (or use a page-size override) to force at - least two pages; confirm both clients follow the cursor and the - combined listing has no gaps or duplicates. -- **[M]** Search for the run prefix; results contain the uploads. -- Semantic search: settings shows semantic enabled with a nonzero - indexed-chunk count once the disposable text upload has been indexed - (wait out one or two 5-minute cron ticks); a conceptual query for the - text file's _content_ (words related to, but not literally in, its - text) surfaces it. SKIP with reason if indexing hasn't caught up - within a reasonable wait. -- **[M]** Rename one file (dashboard or CLI) and confirm the new name. -- **[M]** Create a prefixed tag, add it to a file, filter by it, remove - it. -- **[M]** Upload a replacement version to one file; version history - shows both versions; download the current and the older version and - verify each checksum against the right source bytes. -- Upload enough versions to one file (or use `versionsLimit=1`) to force - version-history pagination; follow the cursor and confirm no gaps or - duplicates across pages. -- **[M]** Trash a file, confirm it lists under trash, restore it, - confirm it's back. -- **[M]** Purge one file and confirm it is gone from list and trash. - -## 5. Public/private behavior - -- **[M]** A public file's content URL (`/f/`) loads with - **no** credentials (fresh private browser context or plain curl). -- **[M]** A private file's content URL without a grant returns 404. -- **[M]** A private file downloads through the dashboard (grant flow - works). -- **[M]** Grant integrity — three separate checks, each rejected: - - a grant past its expiry (wait out a short-lived grant, or note the - TTL makes this impractical and SKIP with reason), - - a granted URL with the expiry parameter altered, - - a granted URL with the signature parameter altered. -- **[M]** Upload a small prefixed HTML file: it must be forced public - and must render only on the content origin. -- **[M]** `GET /api/files` returns 421 (dashboard APIs rejected - on the content origin). -- **[M]** `GET /f/` returns 421 (content rejected on the - dashboard origin). - -## 6. Content and preview validation - -- **[M]** Markdown preview renders for a prefixed .md upload; a link - like `[x](javascript:alert(1))` in it must not produce a clickable - javascript: link, and raw `