From 98a1994f425ed1150c3bda70cabafda221788b59 Mon Sep 17 00:00:00 2001 From: Michael Burr Date: Thu, 12 Nov 2026 20:50:31 +0000 Subject: [PATCH 1/2] build(wrangler): run local dev and local D1 access without wrangler.toml --- e2e/d1Local.ts | 21 ++++ e2e/global-teardown.ts | 28 ++--- e2e/tests/collab.spec.ts | 16 ++- e2e/tests/ink-features.spec.ts | 11 +- e2e/tests/profile-card.spec.ts | 9 +- packages/admin/package.json | 2 +- packages/admin/wrangler.toml | 100 ----------------- packages/api/package.json | 2 +- packages/api/scripts/backfill-logo-keys.ts | 26 +++-- packages/api/src/collab/sizeCap.test.ts | 19 ++-- packages/api/wrangler.toml | 117 -------------------- packages/auth/package.json | 2 +- packages/auth/wrangler.toml | 118 --------------------- packages/frontend/wrangler.toml | 45 -------- scripts/d1-local.mjs | 101 ++++++++++++++++++ 15 files changed, 174 insertions(+), 443 deletions(-) create mode 100644 e2e/d1Local.ts delete mode 100644 packages/admin/wrangler.toml delete mode 100644 packages/api/wrangler.toml delete mode 100644 packages/auth/wrangler.toml delete mode 100644 packages/frontend/wrangler.toml create mode 100644 scripts/d1-local.mjs diff --git a/e2e/d1Local.ts b/e2e/d1Local.ts new file mode 100644 index 0000000..51b3dbc --- /dev/null +++ b/e2e/d1Local.ts @@ -0,0 +1,21 @@ +import { execFileSync } from "node:child_process"; +import { resolve } from "node:path"; + +const SCRIPT = resolve(__dirname, "../scripts/d1-local.mjs"); + +/** + * Run SQL against the local dev D1 (the shared .wrangler/state behind + * `pnpm dev`) via scripts/d1-local.mjs - the config-free replacement for + * `npx wrangler d1 execute --local`. `main` = cubedocs-main (api DB), + * `auth` = cubedocs-auth. Needs the api dev server (:8787) running, which every + * e2e spec already does. Spawned without a shell, so SQL quotes pass through + * verbatim. Throws with the helper's error message on failure. + */ +export function d1Local(db: "main" | "auth", sql: string): void { + try { + execFileSync(process.execPath, [SCRIPT, db, sql], { stdio: "pipe" }); + } catch (err) { + const stderr = (err as { stderr?: Buffer }).stderr?.toString().trim(); + throw new Error(stderr || (err as Error).message); + } +} diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts index f073e50..cd31c22 100644 --- a/e2e/global-teardown.ts +++ b/e2e/global-teardown.ts @@ -1,5 +1,4 @@ -import { execSync } from "node:child_process"; -import { resolve } from "node:path"; +import { d1Local } from "./d1Local"; // Tests that fail mid-flow leave dormant rows behind: a user the spec couldn't // log back into for cleanup, a project whose owner-delete never ran, etc. @@ -8,22 +7,17 @@ import { resolve } from "node:path"; // // This hook is the safety net: after every spec finishes, wipe any row that // was clearly created by the e2e suite. It runs as a Node process (no Page), -// so we shell out to wrangler. It also runs on Playwright failure exits. +// so it writes the local D1 directly (scripts/d1-local.mjs, via the running +// api dev server). It also runs on Playwright failure exits. // // FK cascades handle most of the auth-DB tail (sessions, webauthn, backup // codes, moderation events). email_verification_tokens has no cascade, so we // delete it explicitly first. On the API DB, deleting projects cascades // everything below (docs, folders, files, members, invite links). export default async function globalTeardown() { - const authDir = resolve(__dirname, "../packages/auth"); - const apiDir = resolve(__dirname, "../packages/api"); - - const run = (cwd: string, db: string, sql: string) => { + const run = (db: "main" | "auth", sql: string) => { try { - execSync( - `npx wrangler d1 execute ${db} --local --persist-to ../../.wrangler/state --command "${sql}"`, - { cwd, stdio: "pipe" }, - ); + d1Local(db, sql); } catch (err) { // Dev stack might be down (we just torched it with the test run); that's // fine - there's nothing to clean if there's no DB to talk to. @@ -33,22 +27,16 @@ export default async function globalTeardown() { // Auth DB: orphan tokens first, then users (cascades the rest). run( - authDir, - "cubedocs-auth", + "auth", "DELETE FROM email_verification_tokens WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'e2e-%@example.com');", ); - run( - authDir, - "cubedocs-auth", - "DELETE FROM users WHERE email LIKE 'e2e-%@example.com';", - ); + run("auth", "DELETE FROM users WHERE email LIKE 'e2e-%@example.com';"); // API DB: delete e2e-suite projects by name prefix (cascades everything). // Patterns track the names used in app.spec.ts, invites.spec.ts, and // limited-permissions.spec.ts. run( - apiDir, - "cubedocs-main", + "main", "DELETE FROM projects WHERE name LIKE 'E2E Project %' OR name LIKE 'E2E Scroll %' OR name LIKE 'Invite Test %' OR name LIKE 'Limited Perm Test %';", ); } diff --git a/e2e/tests/collab.spec.ts b/e2e/tests/collab.spec.ts index 850f7fc..04e746c 100644 --- a/e2e/tests/collab.spec.ts +++ b/e2e/tests/collab.spec.ts @@ -18,13 +18,13 @@ * clientID, so two tabs of the same user are still distinct peers, which is * enough to exercise the DocCollabRoom WebSocket fan-out + persistence path. * - * The REALTIME bit (= 4) on `projects.features` has no API toggle; we shell - * out to wrangler the same way the in-package size-cap integration test does. + * The REALTIME bit (= 4) on `projects.features` has no API toggle; we write + * the local D1 directly (e2e/d1Local.ts) the same way the in-package size-cap + * integration test does. */ import { test, expect, type BrowserContext, type Page } from "@playwright/test"; -import { execSync } from "node:child_process"; -import { resolve } from "node:path"; +import { d1Local } from "../d1Local"; const RUN_ID = Date.now(); const EMAIL = `e2e-collab-${RUN_ID}@example.com`; @@ -69,11 +69,7 @@ async function injectFakeIp(ctx: BrowserContext) { } function enableRealtime(pId: string): void { - const apiDir = resolve(__dirname, "../../packages/api"); - execSync( - `npx wrangler d1 execute cubedocs-main --local --persist-to ../../.wrangler/state --command "UPDATE projects SET features = features | 4 WHERE id = '${pId}';"`, - { cwd: apiDir, stdio: "pipe" }, - ); + d1Local("main", `UPDATE projects SET features = features | 4 WHERE id = '${pId}';`); } // Reads the editor's plain text from a page. Uses CodeMirror's `.cm-content` @@ -176,7 +172,7 @@ test("sets up a project + doc with the realtime flag enabled", async () => { // Hard reload so the project's `features` field (which the DocsLayout // outlet computes `realtimeEnabled` from) re-fetches and picks up the bit - // we just flipped via wrangler. + // we just flipped in the local D1. await pageA.reload(); await expect(pageA.locator(".cm-content")).toBeVisible({ timeout: 10000 }); }); diff --git a/e2e/tests/ink-features.spec.ts b/e2e/tests/ink-features.spec.ts index f7cb93c..75baee6 100644 --- a/e2e/tests/ink-features.spec.ts +++ b/e2e/tests/ink-features.spec.ts @@ -11,7 +11,7 @@ * resolves to "ink". Two ways to get there in dev: * 1. Stripe checkout (requires external service, not viable in tests) * 2. Grant the plan directly in the auth DB via `granted_plan = 'ink'` on - * user_billing - same wrangler-shell-out trick collab.spec.ts uses for + * user_billing - same local-D1 write (e2e/d1Local.ts) collab.spec.ts uses for * the REALTIME feature bit * * After granting, the user must re-fetch their session for the resolved plan @@ -20,8 +20,7 @@ */ import { test, expect, type BrowserContext, type Page } from "@playwright/test"; -import { execSync } from "node:child_process"; -import { resolve } from "node:path"; +import { d1Local } from "../d1Local"; const RUN_ID = Date.now(); const EMAIL = `e2e-ink-${RUN_ID}@example.com`; @@ -55,12 +54,8 @@ async function injectFakeIp(ctx: BrowserContext) { } function grantInk(uid: string) { - const authDir = resolve(__dirname, "../../packages/auth"); const sql = `INSERT INTO user_billing (user_id, granted_plan, granted_plan_started_at) VALUES ('${uid}', 'ink', ${Date.now()}) ON CONFLICT(user_id) DO UPDATE SET granted_plan = 'ink', granted_plan_started_at = ${Date.now()};`; - execSync( - `npx wrangler d1 execute cubedocs-auth --local --persist-to ../../.wrangler/state --command "${sql}"`, - { cwd: authDir, stdio: "pipe" }, - ); + d1Local("auth", sql); } test.describe.configure({ mode: "serial" }); diff --git a/e2e/tests/profile-card.spec.ts b/e2e/tests/profile-card.spec.ts index a4105c6..6e407cd 100644 --- a/e2e/tests/profile-card.spec.ts +++ b/e2e/tests/profile-card.spec.ts @@ -16,8 +16,7 @@ */ import { test, expect, type BrowserContext, type Page } from "@playwright/test"; -import { execSync } from "node:child_process"; -import { resolve } from "node:path"; +import { d1Local } from "../d1Local"; const RUN_ID = Date.now(); const EMAIL = `e2e-prof-${RUN_ID}@example.com`; @@ -51,12 +50,8 @@ async function injectFakeIp(ctx: BrowserContext) { } function grantInk(uid: string) { - const authDir = resolve(__dirname, "../../packages/auth"); const sql = `INSERT INTO user_billing (user_id, granted_plan, granted_plan_started_at) VALUES ('${uid}', 'ink', ${Date.now()}) ON CONFLICT(user_id) DO UPDATE SET granted_plan = 'ink', granted_plan_started_at = ${Date.now()};`; - execSync( - `npx wrangler d1 execute cubedocs-auth --local --persist-to ../../.wrangler/state --command "${sql}"`, - { cwd: authDir, stdio: "pipe" }, - ); + d1Local("auth", sql); } test.describe.configure({ mode: "serial" }); diff --git a/packages/admin/package.json b/packages/admin/package.json index 5f8bb3b..53a9cdc 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "concurrently \"cd frontend && pnpm dev\" \"pnpm exec wrangler dev --port 8789 --persist-to ../../.wrangler/state\"", + "dev": "concurrently \"cd frontend && pnpm dev\" \"pnpm exec wrangler dev --experimental-new-config --port 8789 --persist-to ../../.wrangler/state\"", "build": "cd frontend && pnpm build", "deploy": "pnpm build && cf deploy", "typecheck": "tsc --noEmit", diff --git a/packages/admin/wrangler.toml b/packages/admin/wrangler.toml deleted file mode 100644 index b408aec..0000000 --- a/packages/admin/wrangler.toml +++ /dev/null @@ -1,100 +0,0 @@ -# NOTE: `cf build` / `cf deploy` (and Workers Builds) read ./cloudflare.config.ts -# (+ ./wrangler.config.ts for build settings) - that is the deploy source of truth. -# This file is kept for Wrangler commands that still need a config: local dev -# (`wrangler dev --persist-to ../../.wrangler/state`, because `cf dev` can't share -# one persist dir across workers yet) and `npx wrangler d1 execute --local` -# used by the e2e suites and ops scripts. Keep every binding/var change mirrored -# in both files. - -name = "annex-admin" -main = "src/index.ts" -# Keep in lockstep with the auth worker's compatibility_date: this worker -# imports auth source (session.ts/jwt.ts) cross-package, so runtime-behavior -# differences between dates would make shared code behave differently per worker. -compatibility_date = "2026-02-24" -workers_dev = false - -# Hourly: performs account deletions scheduled from the Users page once their -# grace window has passed (src/lib/userDeletion.ts runDueUserDeletions). -[triggers] -crons = ["17 * * * *"] - -[assets] -directory = "./dist" -binding = "SITE_ASSETS" -html_handling = "auto-trailing-slash" -not_found_handling = "single-page-application" - -[[d1_databases]] -binding = "DB" -database_name = "cubedocs-main" -database_id = "267490be-7141-4d00-ab23-152742f2f6ef" - -# Read+write: unlike the API worker's read-only-by-convention AUTH_DB -# binding, the admin worker writes users (moderation/force_password_change), -# user_moderation_events, user_preferences (badges), user_billing (grant -# columns), and admin_audit_log directly. Credential/session/oauth_clients -# writes still go through the AUTH service binding. Migrations live only in -# packages/auth/migrations/. -[[d1_databases]] -binding = "AUTH_DB" -database_name = "cubedocs-auth" -database_id = "db327d85-b8c2-4fed-adaa-25e748778c86" - -[[r2_buckets]] -binding = "ASSETS" -bucket_name = "cubedocs-assets" - -[[services]] -binding = "AUTH" -service = "annex-auth" - -[[routes]] -pattern = "admin.cubityfir.st" -custom_domain = true - -[vars] -# Cloudflare-for-SaaS custom domains - used to release a site's custom hostname -# when an admin removes a domain or deletes the site (mirrors the api worker). -# MUST match the api worker's values (the dedicated yourannex.com zone - see -# memories/Custom-Domains.md), and CF_API_TOKEN must be scoped to that zone. -# When CF_ZONE_ID / CF_API_TOKEN are unset, the CF call is a safe no-op (the DB -# row is still removed). -CF_ZONE_ID = "397deb54a68d306201a295d1793fe84c" -CUSTOM_DOMAIN_CNAME_TARGET = "cname.yourannex.com" - -# GA [[ratelimits]] binding (not the deprecated [[unsafe.bindings]] form) - -# same behavior, but simulated locally in wrangler dev. See auth wrangler.toml. -[[ratelimits]] -name = "RATE_LIMITER_ADMIN" -namespace_id = "2001" -# IP-keyed coarse abuse cap across every authenticated admin API route. -# Generous enough for an operator clicking around the dashboard; only -# bites scripted abuse with a stolen admin token. -simple = { limit = 60, period = 60 } - -[[ratelimits]] -name = "RATE_LIMITER_ADMIN_HANDOFF" -namespace_id = "2002" -# Tighter, separate bucket for the unauthenticated handoff-exchange -# proxy (the only pre-auth endpoint that hits the auth worker + DB). -simple = { limit = 10, period = 60 } - -[[ratelimits]] -name = "RATE_LIMITER_AVATAR" -namespace_id = "2003" -# Separate budget for the public avatar route: a 25-row dashboard page -# costs 25 avatar fetches, which previously drained the shared admin -# API budget and started 429ing the operator after ~2 searches/min. -simple = { limit = 300, period = 60 } - -# Secrets (set via `wrangler secret put`): -# JWT_SECRET (same value as the auth worker's JWT_SECRET - the -# admin worker verifies sessions inline against -# AUTH_DB instead of calling auth's /verify route) -# STRIPE_SECRET_KEY (sk_test_... or sk_live_... - same value as auth -# worker; only used for admin-driven cancellations -# triggered from the user details panel) -# CF_API_TOKEN (same token as the api worker - zone-scoped, SSL and -# Certificates: Edit; only used to release a custom -# hostname on admin-driven site delete) diff --git a/packages/api/package.json b/packages/api/package.json index 119d50d..132e9ab 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm exec wrangler dev --port 8787 --persist-to ../../.wrangler/state", + "dev": "pnpm exec wrangler dev --experimental-new-config --port 8787 --persist-to ../../.wrangler/state", "migrate": "cf d1 migrations apply 267490be-7141-4d00-ab23-152742f2f6ef --local --persist-to ../../.wrangler/state", "build": "tsc", "deploy": "cf deploy", diff --git a/packages/api/scripts/backfill-logo-keys.ts b/packages/api/scripts/backfill-logo-keys.ts index 9cac281..3e6281e 100644 --- a/packages/api/scripts/backfill-logo-keys.ts +++ b/packages/api/scripts/backfill-logo-keys.ts @@ -6,7 +6,9 @@ * * Run from the monorepo root AFTER applying migration 0048: * - * # against the local persisted dev state + * # against the local persisted dev state (the api dev server must be + * # running - `pnpm dev` - because the local D1 read goes through + * # scripts/d1-local.mjs) * npx tsx packages/api/scripts/backfill-logo-keys.ts --local * * # against prod (uses your default wrangler login) @@ -35,23 +37,31 @@ if (isLocal === isRemote) { process.exit(1); } -// `--config` so we can run from monorepo root and still resolve the -// `cubedocs-main` D1 binding + `cubedocs-assets` R2 binding declared in -// the api package's wrangler.toml. -const configFlags = ["--config", "packages/api/wrangler.toml"]; +// No Wrangler config file exists (the Worker config is cloudflare.config.ts): +// `wrangler r2 object` and `wrangler d1 execute --remote` address the bucket / +// database by name without one. `wrangler d1 execute --local` cannot, so the +// local D1 read goes through scripts/d1-local.mjs instead. const baseFlags = isLocal - ? [...configFlags, "--local", "--persist-to", "./.wrangler/state"] - : [...configFlags, "--remote"]; + ? ["--local", "--persist-to", "./.wrangler/state"] + : ["--remote"]; function wrangler(...subargs: string[]): string { // shell: true so this works on Windows where `npx` is `npx.cmd`. return execFileSync("npx", ["wrangler", ...subargs], { encoding: "utf8", shell: true }); } +const LIST_SQL = "SELECT id FROM projects WHERE logo_wide_updated_at IS NOT NULL"; + function listProjectIdsWithLogo(): string[] { + if (isLocal) { + // d1-local prints one JSON line per statement: { columns, rows }. + const out = execFileSync(process.execPath, ["scripts/d1-local.mjs", "main", LIST_SQL], { encoding: "utf8" }); + const { rows } = JSON.parse(out.trim().split("\n").pop()!) as { rows: string[][] }; + return rows.map(r => r[0]); + } const out = wrangler( "d1", "execute", DB, - "--command", `"SELECT id FROM projects WHERE logo_wide_updated_at IS NOT NULL"`, + "--command", `"${LIST_SQL}"`, "--json", ...baseFlags, ); diff --git a/packages/api/src/collab/sizeCap.test.ts b/packages/api/src/collab/sizeCap.test.ts index e81f5c0..33eac35 100644 --- a/packages/api/src/collab/sizeCap.test.ts +++ b/packages/api/src/collab/sizeCap.test.ts @@ -3,15 +3,17 @@ * * Prerequisites: * 1. Start both workers from the repo root: pnpm dev - * 2. wrangler CLI must be available from packages/api (already a dev dep) - the test - * uses it to flip the REALTIME flag on the test project, since there's no API - * route for that today (see CLAUDE.md, "Enabling for a project locally"). + * 2. The test flips the REALTIME flag on the test project by writing the local D1 + * through scripts/d1-local.mjs (via the running api dev server), since there's + * no API route for that today (see memories/Collab.md, "Enabling for a project + * locally"). * * Tests skip automatically when the dev API isn't reachable. */ import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import * as Y from "yjs"; import * as encoding from "lib0/encoding"; @@ -49,10 +51,13 @@ function encodeSyncStep2(update: Uint8Array): Uint8Array { } // Flip the REALTIME bit (= 4) on a project's `features` column. There's no API route for -// this - production toggling happens via an admin script - so the test shells out to wrangler. +// this - production toggling happens via an admin script - so the test writes the local D1 +// directly with scripts/d1-local.mjs (no shell, so the SQL quotes pass through verbatim). +const D1_LOCAL = fileURLToPath(new URL("../../../../scripts/d1-local.mjs", import.meta.url)); function enableRealtime(projectId: string): void { - execSync( - `npx wrangler d1 execute cubedocs-main --local --persist-to ../../.wrangler/state --command "UPDATE projects SET features = features | 4 WHERE id = '${projectId}';"`, + execFileSync( + process.execPath, + [D1_LOCAL, "main", `UPDATE projects SET features = features | 4 WHERE id = '${projectId}';`], { stdio: "pipe" }, ); } diff --git a/packages/api/wrangler.toml b/packages/api/wrangler.toml deleted file mode 100644 index 8969e8a..0000000 --- a/packages/api/wrangler.toml +++ /dev/null @@ -1,117 +0,0 @@ -# NOTE: `cf build` / `cf deploy` (and Workers Builds) read ./cloudflare.config.ts -# (+ ./wrangler.config.ts for build settings) - that is the deploy source of truth. -# This file is kept for Wrangler commands that still need a config: local dev -# (`wrangler dev --persist-to ../../.wrangler/state`, because `cf dev` can't share -# one persist dir across workers yet) and `npx wrangler d1 execute --local` -# used by the e2e suites and ops scripts. Keep every binding/var change mirrored -# in both files. - -name = "annex-api" -main = "src/index.ts" -compatibility_date = "2026-02-24" -compatibility_flags = ["nodejs_compat"] -workers_dev = false - -# Daily revision-retention sweep (pruneDocRevisions in lib/docOps.ts): doc -# revisions older than 90 days are pruned, always keeping each doc's newest. -[triggers] -crons = ["0 4 * * *"] - -[[durable_objects.bindings]] -name = "DOC_COLLAB" -class_name = "DocCollabRoom" - -[[migrations]] -tag = "v1" -new_classes = ["DocCollabRoom"] - -[[d1_databases]] -binding = "DB" -database_name = "cubedocs-main" -database_id = "267490be-7141-4d00-ab23-152742f2f6ef" -migrations_dir = "migrations" - -# Read-only access to the auth DB so this worker can verify sessions inline -# (one D1 batch read of users + sessions) instead of round-tripping through -# the auth worker's /verify route on every request. Migrations live with the -# auth worker - DO NOT add migrations_dir here. -[[d1_databases]] -binding = "AUTH_DB" -database_name = "cubedocs-auth" -database_id = "db327d85-b8c2-4fed-adaa-25e748778c86" - -[[r2_buckets]] -binding = "ASSETS" -bucket_name = "cubedocs-assets" - -# Service binding to auth worker (zero-latency internal calls) -[[services]] -binding = "AUTH" -service = "annex-auth" - -[vars] -ENVIRONMENT = "production" -# Cloudflare for SaaS custom domains (see memories/Custom-Domains.md). These live -# on the DEDICATED `yourannex.com` zone, kept separate from cubityfir.st so the -# SaaS `*/*` catch-all route can't shadow our own hosts (it once hijacked -# i.cubityfir.st). CUSTOM_DOMAIN_CNAME_TARGET is the proxied hostname customers -# CNAME their domain to; CF_ZONE_ID is the yourannex.com zone id. When CF_ZONE_ID -# or CF_API_TOKEN is unset, the custom-domain endpoints report "not configured" -# and never call Cloudflare. -CUSTOM_DOMAIN_CNAME_TARGET = "publish.yourannex.com" -CF_ZONE_ID = "397deb54a68d306201a295d1793fe84c" - -# Direct-from-R2 presigned video streaming (see lib/r2Presign.ts). The bucket -# name matches the [[r2_buckets]] binding above. Set R2_ACCOUNT_ID to your CF -# account id, and the two secrets via: -# npx wrangler secret put R2_ACCESS_KEY_ID -# npx wrangler secret put R2_SECRET_ACCESS_KEY -# (create an R2 API token with Object Read for cubedocs-assets). When any of the -# four is unset, video falls back to the in-Worker token streaming route. -R2_BUCKET_NAME = "cubedocs-assets" -R2_ACCOUNT_ID = "d843ae90cab33e54f4d284bc65d2fd6a" - -# Per-user limiter for the invite-by-email lookup. The auth worker's -# /lookup also has its own coarser IP-keyed limiter - this one caps any -# single account from scraping the email→user map even when multiple -# users share an IP (office NAT). -# GA [[ratelimits]] binding (not the deprecated [[unsafe.bindings]] form) - -# same behavior, but simulated locally in wrangler dev. See auth wrangler.toml. -[[ratelimits]] -name = "RATE_LIMITER_INVITE_LOOKUP" -namespace_id = "2001" -simple = { limit = 8, period = 60 } - -# Per-API-key limiter for the public /v1 surface, keyed by `apikey:`. -# Caps abuse / runaway scripts on a single scoped key without touching the -# interactive JWT routes. -[[ratelimits]] -name = "RATE_LIMITER_API" -namespace_id = "2002" -simple = { limit = 120, period = 60 } - -# Per-IP limiter for the public "Report Site" endpoint (routes/public.ts -# handlePublicReport). Reports are rare, deliberate actions - a human files -# one or two; anything faster is a script. -[[ratelimits]] -name = "RATE_LIMITER_REPORT" -namespace_id = "2003" -simple = { limit = 3, period = 60 } - -# Cloudflare Flagship feature flags. Shared app with the auth worker. Flags in use: -# api (default on) - killswitch for the whole /v1 public API surface (v1.ts). -# custom-theming (default off) - per-user gate for custom themes (index.ts). -# ai-summaries (default on) - killswitch for the OpenAI summarize endpoint (routes/ai.ts). -# realtime-collab (default on) - killswitch for new collab WS connections (handleCollabUpgrade). -# NOTE: wrangler dev DOES serve this binding locally with the real flag state, so a -# flag flipped in the Flagship dashboard affects local dev too. The per-call code -# defaults only apply when the binding itself is missing. -[[flagship]] -binding = "FLAGS" -app_id = "08fb610f-b8ed-4a60-afcf-7f5fe6b9f948" - -# Secrets (set via `wrangler secret put`): -# JWT_SECRET (same value as auth worker - used for local JWT verification) -# OPENAI_API_KEY (OpenAI API key for AI summarisation feature) -# CF_API_TOKEN (Cloudflare token scoped to the zone with "SSL and -# Certificates: Edit" - used to create/poll custom hostnames) diff --git a/packages/auth/package.json b/packages/auth/package.json index 05eca29..703b84c 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm exec wrangler dev --port 8788 --persist-to ../../.wrangler/state", + "dev": "pnpm exec wrangler dev --experimental-new-config --port 8788 --persist-to ../../.wrangler/state", "migrate": "cf d1 migrations apply db327d85-b8c2-4fed-adaa-25e748778c86 --local --persist-to ../../.wrangler/state", "build": "tsc", "deploy": "cf deploy", diff --git a/packages/auth/wrangler.toml b/packages/auth/wrangler.toml deleted file mode 100644 index 6580e6f..0000000 --- a/packages/auth/wrangler.toml +++ /dev/null @@ -1,118 +0,0 @@ -# NOTE: `cf build` / `cf deploy` (and Workers Builds) read ./cloudflare.config.ts -# (+ ./wrangler.config.ts for build settings) - that is the deploy source of truth. -# This file is kept for Wrangler commands that still need a config: local dev -# (`wrangler dev --persist-to ../../.wrangler/state`, because `cf dev` can't share -# one persist dir across workers yet) and `npx wrangler d1 execute --local` -# used by the e2e suites and ops scripts. Keep every binding/var change mirrored -# in both files. - -name = "annex-auth" -main = "src/index.ts" -compatibility_date = "2026-02-24" -compatibility_flags = ["nodejs_compat"] -workers_dev = false - -[[d1_databases]] -binding = "DB" -database_name = "cubedocs-auth" -database_id = "db327d85-b8c2-4fed-adaa-25e748778c86" -migrations_dir = "migrations" - -[triggers] -# Weekly sweep of long-dead session rows (see scheduled handler). -crons = ["0 3 * * SUN"] - -# "Sign in with Annex" OIDC provider - public endpoints on a DEDICATED identity -# host so the issuer is decoupled from the app origin (docs.cubityfir.st), which -# may migrate. Only /oauth/* and /.well-known/* are exposed here; nothing else on -# the auth worker is reachable at auth.cubityfir.st. Requires a proxied DNS record -# for `auth.cubityfir.st` on the cubityfir.st zone (see docs/oauth/README.md). -# The browser-facing authorization page lives on the app origin (OIDC_AUTHORIZE_URL). -[[routes]] -pattern = "auth.cubityfir.st/oauth/*" -zone_name = "cubityfir.st" - -[[routes]] -pattern = "auth.cubityfir.st/.well-known/*" -zone_name = "cubityfir.st" - -[vars] -JWT_ISSUER = "https://auth.cubedocs.com" -ADMIN_APP_ORIGIN = "https://admin.cubityfir.st" -APP_ORIGIN = "https://docs.cubityfir.st" -# "Sign in with Annex" OIDC provider. OIDC_ISSUER is the `iss` of every token and -# the base for discovery (https://auth.cubityfir.st/.well-known/openid-configuration). -# OIDC_AUTHORIZE_URL is the browser-facing authorize page on the app origin (the -# user's Annex session lives there, not on the issuer host). -OIDC_ISSUER = "https://auth.cubityfir.st" -OIDC_AUTHORIZE_URL = "https://docs.cubityfir.st/oauth/authorize" -WEBAUTHN_RP_ID = "docs.cubityfir.st" -WEBAUTHN_RP_NAME = "Annex" -WEBAUTHN_ORIGIN = "https://docs.cubityfir.st" -# Stripe price id for the Annex Ink supporter tier ($5/mo recurring). -# Not a secret - safe to commit. Set the live id once the Stripe product -# is created in the dashboard. -STRIPE_INK_PRICE_ID = "price_1TVJk5Rckd4Ls9nPHpnY4tAN" - -[[send_email]] -name = "EMAIL" -allowed_sender_addresses = ["noreply@docs.cubityfir.st"] - -# Cloudflare Flagship feature flags. Shares the same app as the API worker so a -# single source governs both. Flags in use: -# signup (default on) - when false, /register is refused (handleRegister). -# email-verification (default off) - gates registration verification emails, -# blocks unverified logins, and selects the -# confirm-first email-change flow (register.ts / -# login.ts / change-email.ts). -# NOTE: wrangler dev DOES serve this binding locally with the real flag state, -# so a flag flipped in the Flagship dashboard affects local dev too (e.g. a -# disabled `signup` flag blocks the e2e suites, which register accounts). The -# per-call code defaults only apply when the binding itself is missing. -# Exception: `email-verification` is never enforced on a localhost deployment - -# isEmailVerificationEnabled (verification.ts) bypasses it so dashboard flips -# can't break local login/e2e; set DEV_EMAIL_VERIFICATION=true in .dev.vars to -# opt back in. -[[flagship]] -binding = "FLAGS" -app_id = "08fb610f-b8ed-4a60-afcf-7f5fe6b9f948" - -# Rate limiters use the GA [[ratelimits]] binding (not the deprecated -# [[unsafe.bindings]] form): identical runtime behavior and namespace ids, but -# wrangler dev simulates it locally instead of proxying to Cloudflare - the -# remote proxy breaks requests that arrive via cross-process service bindings. -[[ratelimits]] -name = "RATE_LIMITER_LOOKUP" -namespace_id = "1001" -# Coarse abuse cap, IP-keyed. Per-user limiting for authenticated callers -# happens on the API worker (see RATE_LIMITER_INVITE_LOOKUP there). -simple = { limit = 30, period = 60 } - -[[ratelimits]] -name = "RATE_LIMITER_AUTH" -namespace_id = "1002" -simple = { limit = 10, period = 60 } - -[[ratelimits]] -name = "RATE_LIMITER_EMAIL_VERIFY" -namespace_id = "1003" -simple = { limit = 3, period = 60 } - -# OIDC token endpoint (/oauth/token). Server-to-server, so a single connected -# service exchanges every user's code from one egress IP - needs a far higher -# ceiling than the interactive login limiter. IP-keyed (CF-Connecting-IP is real -# on the directly-routed auth.cubityfir.st host). -[[ratelimits]] -name = "RATE_LIMITER_OIDC" -namespace_id = "1004" -simple = { limit = 100, period = 60 } - -# Secrets (set via `wrangler secret put`): -# JWT_SECRET -# TURNSTILE_SECRET -# STRIPE_SECRET_KEY (sk_test_... or sk_live_...) -# STRIPE_WEBHOOK_SECRET (whsec_... - generated by Stripe when you -# register the /stripe/webhook endpoint) -# OIDC_PRIVATE_KEY (RS256 signing key - RSA private JWK as a JSON string. -# Generate with `node scripts/gen-oidc-key.mjs`. The -# public half is published at /oauth/jwks.) diff --git a/packages/frontend/wrangler.toml b/packages/frontend/wrangler.toml deleted file mode 100644 index a58a0b3..0000000 --- a/packages/frontend/wrangler.toml +++ /dev/null @@ -1,45 +0,0 @@ -# NOTE: `cf build` / `cf deploy` (and Workers Builds) read ./cloudflare.config.ts -# (+ ./wrangler.config.ts for build settings) - that is the deploy source of truth. -# This file is kept for Wrangler commands that still need a config: local dev -# (`wrangler dev --persist-to ../../.wrangler/state`, because `cf dev` can't share -# one persist dir across workers yet) and `npx wrangler d1 execute --local` -# used by the e2e suites and ops scripts. Keep every binding/var change mirrored -# in both files. - -name = "annex-frontend" -main = "worker/index.ts" -compatibility_date = "2026-02-24" - -[assets] -directory = "./dist" -binding = "ASSETS" - -[[services]] -binding = "API" -service = "annex-api" - -# Service binding to the auth worker - used only for Stripe webhook -# forwarding (/stripe/webhook). The auth worker is otherwise reached -# via the api worker's own AUTH binding for authenticated calls. -[[services]] -binding = "AUTH" -service = "annex-auth" - -[[routes]] -pattern = "docs.cubityfir.st" -custom_domain = true - -# Cloudflare-for-SaaS catch-all, on the DEDICATED `yourannex.com` zone. Customer -# custom domains (their.domain -> publish.yourannex.com) arrive as custom hostnames -# on that zone; this `*/*` route hands them to annex-frontend in host-mode. -# Required - without it, custom hostnames hit the originless fallback origin and -# return 522. The zone is dedicated to SaaS routing (nothing else lives on it), -# so `*/*` is safe here: it can't shadow our own infra the way it did on -# cubityfir.st (where it once hijacked i.cubityfir.st). The app itself still -# serves on docs.cubityfir.st via the exact custom_domain route above. -# NOTE: the OLD `*/*` route on the cubityfir.st zone must be deleted in the -# dashboard - wrangler only manages the routes in this file and won't remove it. -# See memories/Custom-Domains.md. -[[routes]] -pattern = "*/*" -zone_name = "yourannex.com" diff --git a/scripts/d1-local.mjs b/scripts/d1-local.mjs new file mode 100644 index 0000000..a722d98 --- /dev/null +++ b/scripts/d1-local.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +// Run SQL against the LOCAL dev D1 databases (the shared .wrangler/state that +// `pnpm dev` serves) - the replacement for +// `npx wrangler d1 execute --local --persist-to ../../.wrangler/state`. +// +// Why not wrangler: the Worker config is cloudflare.config.ts only (there is no +// wrangler.toml), and `wrangler d1 execute --local` can only resolve a local +// database through a Wrangler config file. `cf d1 query --local` has no local +// implementation yet either. So this goes through the running api dev server's +// Local Explorer API (`/cdn-cgi/local/explorer/api/d1/database//raw`), +// which binds both databases (DB = cubedocs-main, AUTH_DB = cubedocs-auth) and +// reads/writes the exact same SQLite files as the dev servers. +// +// Requires the api worker to be running (`pnpm dev`, :8787). Override the +// origin with ANNEX_DEV_API_ORIGIN (e.g. http://127.0.0.1:18787). +// +// CLI: +// node scripts/d1-local.mjs "" +// node scripts/d1-local.mjs --file path/to/file.sql +// Prints each statement's result as JSON ({ columns, rows }). Exits non-zero on +// any SQL or connection error. +// +// Programmatic (from Node/TS tests): +// import { d1Local } from "../scripts/d1-local.mjs"; +// await d1Local("auth", "UPDATE users SET ..."); +// or, from synchronous code, spawn this file with execFileSync(process.execPath, +// [script, db, sql]) - no shell, so SQL quoting is passed through verbatim. + +import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +// Database ids from packages/{api,auth}/cloudflare.config.ts. +export const D1_DATABASES = { + main: "267490be-7141-4d00-ab23-152742f2f6ef", // cubedocs-main + auth: "db327d85-b8c2-4fed-adaa-25e748778c86", // cubedocs-auth +}; + +export function devApiOrigin() { + return (process.env.ANNEX_DEV_API_ORIGIN ?? "http://127.0.0.1:8787").replace(/\/+$/, ""); +} + +/** + * Execute `sql` (one or more `;`-separated statements) against a local D1. + * Resolves to the explorer's per-statement results; rejects on any error. + * @param {"main" | "auth"} db + * @param {string} sql + * @returns {Promise>} + */ +export async function d1Local(db, sql) { + const id = D1_DATABASES[db]; + if (!id) throw new Error(`Unknown database "${db}" (expected "main" or "auth")`); + const url = `${devApiOrigin()}/cdn-cgi/local/explorer/api/d1/database/${id}/raw`; + let res; + try { + res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sql }), + }); + } catch (err) { + throw new Error( + `Could not reach the local api dev server at ${devApiOrigin()} - is \`pnpm dev\` running? (${err.cause?.code ?? err.message})`, + ); + } + const text = await res.text(); + let body; + try { + body = JSON.parse(text); + } catch { + throw new Error(`Unexpected ${res.status} response from ${url}: ${text.slice(0, 200)}`); + } + const failed = !res.ok || !body.success || (body.result ?? []).some((r) => r.success === false); + if (failed) { + const messages = [...(body.errors ?? []), ...(body.result ?? []).flatMap((r) => r.errors ?? [])] + .map((e) => e.message ?? JSON.stringify(e)); + throw new Error(`D1 ${db} query failed (${res.status}): ${messages.join("; ") || text.slice(0, 200)}`); + } + return body.result ?? []; +} + +async function main(argv) { + const [db, ...rest] = argv; + let sql; + if (rest[0] === "--file" && rest[1]) sql = readFileSync(rest[1], "utf8"); + else if (rest.length === 1) sql = rest[0]; + if (!db || !sql) { + console.error('Usage: node scripts/d1-local.mjs "" | --file '); + process.exit(2); + } + try { + const results = await d1Local(db, sql); + for (const r of results) console.log(JSON.stringify(r.results ?? null)); + } catch (err) { + console.error(err.message); + process.exit(1); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(process.argv.slice(2)); +} From 7f7215109e249cf06463af0650c4a655e89958dc Mon Sep 17 00:00:00 2001 From: Michael Burr Date: Thu, 12 Nov 2026 21:47:48 +0000 Subject: [PATCH 2/2] docs: cloudflare.config.ts is the only Worker config --- .claude/wf-auth-audit.mjs | 4 ++-- CLAUDE.md | 20 ++++++++++---------- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/oauth/README.md | 7 ++++--- memories/Api-Keys.md | 2 +- memories/Collab.md | 5 +++-- memories/Custom-Domains.md | 6 +++--- memories/Ink-Stripe.md | 2 +- memories/Organizations.md | 4 ++-- packages/admin/wrangler.config.ts | 9 +++++---- packages/api/wrangler.config.ts | 9 +++++---- packages/auth/wrangler.config.ts | 9 +++++---- scripts/register-oauth-client.mjs | 3 ++- 14 files changed, 46 insertions(+), 40 deletions(-) diff --git a/.claude/wf-auth-audit.mjs b/.claude/wf-auth-audit.mjs index 0b70be1..ea0b654 100644 --- a/.claude/wf-auth-audit.mjs +++ b/.claude/wf-auth-audit.mjs @@ -13,10 +13,10 @@ const CONTEXT = [ 'You are auditing the Annex auth worker (a Cloudflare Worker) at ' + ROOT + '.', 'Base every claim on source you have read, and cite file:line for it.', '', - 'ROUTING / EXPOSURE MODEL (from src/index.ts + wrangler.toml):', + 'ROUTING / EXPOSURE MODEL (from src/index.ts + cloudflare.config.ts):', '- The worker has ONE fetch() router in src/index.ts dispatching by exact pathname+method.', '- PUBLICLY reachable on the internet at auth.cubityfir.st are ONLY: /oauth/token, /oauth/userinfo,', - ' /oauth/jwks, /.well-known/openid-configuration (wrangler [[routes]] only expose /oauth/* and', + ' /oauth/jwks, /.well-known/openid-configuration (cloudflare.config.ts fetch routes only expose /oauth/* and', ' /.well-known/*). These advertise CORS "*". /oauth/authorize is POST-only and reached via the app', ' proxy with a Bearer session token (CORS locked to https://docs.cubityfir.st).', '- Every OTHER route (login, register, totp, webauthn, sessions, billing, stripe webhook, admin/*,', diff --git a/CLAUDE.md b/CLAUDE.md index 5f184eb..f91a8cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,7 +94,7 @@ Annex is an **OpenID Connect identity provider** (authorization-code + PKCE, RS2 id_tokens via JWKS) so other first-party services can offer "Sign in with Annex". Issuer is the dedicated host **`https://auth.cubityfir.st`** (the auth worker serves `/oauth/{token,userinfo,jwks}` + `/.well-known/openid-configuration` there via routes -in its `wrangler.toml`); the browser-facing **authorization_endpoint is a SPA page on +in its `cloudflare.config.ts`); the browser-facing **authorization_endpoint is a SPA page on the app origin** `docs.cubityfir.st/oauth/authorize` because the user's Annex session lives there. **Anything touching `oauth_clients`/`oauth_codes` (`0028_*`), `src/oidc.ts`, `routes/oauth-*.ts`, the `/oauth/authorize` proxy, the `RATE_LIMITER_OIDC` binding, @@ -122,13 +122,14 @@ Use Conventional Commits with a type **and** scope: `(): --local`. **Any binding/var/route change goes in both files.** +- **Local dev** (`pnpm dev`): api/auth/admin run `wrangler dev --experimental-new-config --port --persist-to ../../.wrangler/state`, which loads `cloudflare.config.ts` + `wrangler.config.ts` (and `.dev.vars`) directly. Plain `cf dev` isn't used because it can't take a persist dir yet, and all workers must share one state dir (the api reads auth's DB via `AUTH_DB`). The frontend's `pnpm dev` is plain Vite. +- **Local D1 SQL** (no Wrangler config means `wrangler d1 execute --local` can't resolve a database, and `cf d1 query --local` isn't implemented yet): use `node scripts/d1-local.mjs ""` (or `--file`), which runs through the running api dev server's Local Explorer API against the shared state - see "Local D1 State" below. Remote commands (`npx wrangler d1 execute --remote`, `npx wrangler r2 object ... --remote`) work without a config file. - D1 migrations: `pnpm migrate` / `node scripts/migrate.mjs ` run `cf d1 migrations apply ` (remote unless `--local`). - `annex-api`'s `DocCollabRoom` is declared as `exports.durableObject({ storage: "legacy-kv" })` - it was created with `new_classes` (KV-backed). Never change it to `"sqlite"`. - The frontend package depends on Vite, so `cf build` would run `vite build` instead of Wrangler; its deploy is `pnpm build && pnpm build:worker (cf-wrangler build) && cf deploy --prebuilt`. @@ -138,14 +139,13 @@ Each worker (`packages/{api,auth,admin,frontend}`) is configured by **`cloudflar The dev servers use a shared wrangler state at the **monorepo root** (`/.wrangler/state`), not inside each package. This is set via `--persist-to ../../.wrangler/state` in each package's dev script. -When running local D1 migrations or queries, always pass `--persist-to ../../.wrangler/state` from the package directory, otherwise the command hits the package-local `.wrangler/state` which the dev server never reads. +Local migrations (`pnpm migrate` / `node scripts/migrate.mjs local`) pass `--persist-to ../../.wrangler/state` from the package directory; any manual `cf d1 migrations apply --local` must too, otherwise it hits a state the dev servers never read. -``` -# Correct - targets the shared dev state -cd packages/auth && npx wrangler d1 execute cubedocs-auth --local --persist-to ../../.wrangler/state --command "..." +Ad-hoc local queries go through `scripts/d1-local.mjs` (needs `pnpm dev` running - it talks to the api worker on :8787, override with `ANNEX_DEV_API_ORIGIN`). `main` = `cubedocs-main`, `auth` = `cubedocs-auth`; it prints the last statement's rows as JSON. Use it for DML/SELECTs - schema changes belong in migrations. e2e specs use the `e2e/d1Local.ts` wrapper. -# Wrong - targets packages/auth/.wrangler/state, ignored by dev server -cd packages/auth && npx wrangler d1 execute cubedocs-auth --local --command "..." +``` +node scripts/d1-local.mjs auth "SELECT id, email FROM users LIMIT 5" +node scripts/d1-local.mjs main "UPDATE projects SET features = features | 4 WHERE id = ''" ``` ## Parallel Worktrees (dev review) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c66fa1..7ce73fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ pnpm migrations:local # apply D1 migrations to the shared local dev state pnpm dev # frontend :5173, api :8787, auth :8788, admin :8789 ``` -The dev servers share one wrangler state at the monorepo root (`.wrangler/state`). Any manual `wrangler d1` command must pass `--persist-to ../../.wrangler/state` from the package directory or it will hit a state the dev servers never read. All wrangler invocations go through `npx wrangler`. +The dev servers share one local state at the monorepo root (`.wrangler/state`). Worker config lives only in each package's `cloudflare.config.ts` (+ `wrangler.config.ts` for build settings) - there is no `wrangler.toml`. Local migrations (`pnpm migrate`) pass `--persist-to ../../.wrangler/state` from the package directory; for ad-hoc local SQL use `node scripts/d1-local.mjs ""` while `pnpm dev` is running. All wrangler invocations go through `npx wrangler`. ## Packages diff --git a/README.md b/README.md index e07a738..923c9eb 100644 --- a/README.md +++ b/README.md @@ -197,8 +197,8 @@ npx wrangler secret put R2_SECRET_ACCESS_KEY Enter each value at the interactive prompt (do **not** pipe it in via PowerShell - it appends a CRLF and corrupts the secret). Then set the two vars -in `packages/api/cloudflare.config.ts` (`R2_BUCKET_NAME` is already there), and -mirror them in `packages/api/wrangler.toml` `[vars]` (still used by local dev): +in `packages/api/cloudflare.config.ts` (`R2_BUCKET_NAME` is already there; local +dev reads the same file): ```ts R2_BUCKET_NAME: bindings.text("cubedocs-assets"), diff --git a/docs/oauth/README.md b/docs/oauth/README.md index eef4c2c..1cd406d 100644 --- a/docs/oauth/README.md +++ b/docs/oauth/README.md @@ -67,9 +67,8 @@ Annex secret is ever shared with a connected service. 3. **Apply the migration** (creates `oauth_clients` + `oauth_codes`): ```bash - cd packages/auth - npx wrangler d1 migrations apply cubedocs-auth --remote - # local dev: --local --persist-to ../../.wrangler/state + pnpm migrations:remote # cf d1 migrations apply for both DBs + # local dev: pnpm migrations:local ``` 4. **Deploy** the three workers that gained code (new tables aren't read by @@ -117,6 +116,8 @@ It prints the `client_id` + `client_secret` (**secret shown once**) and writes a ```bash cd packages/auth && npx wrangler d1 execute cubedocs-auth --remote --file +# local dev (with `pnpm dev` running), from the repo root: +node scripts/d1-local.mjs auth --file ``` Either way, put `client_id` + `client_secret` (+ the discovery URL) into the diff --git a/memories/Api-Keys.md b/memories/Api-Keys.md index a34a663..6b04eff 100644 --- a/memories/Api-Keys.md +++ b/memories/Api-Keys.md @@ -92,7 +92,7 @@ hides mutations), but Annex remains the real enforcement. `revoke_key` calls ## Rate limiting & killswitch -- `RATE_LIMITER_API` (wrangler `unsafe.bindings`, namespace `2002`, 120/60s) keyed +- `RATE_LIMITER_API` (`bindings.rateLimit` in `cloudflare.config.ts`, namespace `2002`, 120/60s) keyed by `apikey:`, applied to all `/v1`. Invite email-lookup also reuses `RATE_LIMITER_INVITE_LOOKUP` keyed by owner `userId`. - Flagship flag **`api`** is a global killswitch checked first in `routes/v1.ts` diff --git a/memories/Collab.md b/memories/Collab.md index adaffb7..9689848 100644 --- a/memories/Collab.md +++ b/memories/Collab.md @@ -34,7 +34,8 @@ The API worker hands identity to the DO via `X-User-Id` / `X-User-Name` (URI-enc ## Enabling for a project locally +With `pnpm dev` running (the helper writes the shared local D1 through the api dev server): + ``` -cd packages/api && npx wrangler d1 execute cubedocs-main --local --persist-to ../../.wrangler/state \ - --command "UPDATE projects SET features = features | 4 WHERE id = '';" +node scripts/d1-local.mjs main "UPDATE projects SET features = features | 4 WHERE id = '';" ``` diff --git a/memories/Custom-Domains.md b/memories/Custom-Domains.md index 4de1632..f3a1eb2 100644 --- a/memories/Custom-Domains.md +++ b/memories/Custom-Domains.md @@ -58,11 +58,11 @@ Custom hostnames are created **non-wildcard** (`wildcard: false`) with **TXT DCV 1. **Enable Cloudflare for SaaS** on the `yourannex.com` zone (paid add-on). 2. **CNAME target**: create a proxied record `publish.yourannex.com AAAA 100::` (orange cloud) - the hostname customers point their CNAME at. 3. **Fallback origin**: set the zone's SaaS fallback origin to `publish.yourannex.com` (the same proxied record doubles as the fallback origin, so the apex stays free). Must reach **Active**. -4. **Worker route**: add `*/*` → `annex-frontend` on the `yourannex.com` zone (the "Workers as your fallback origin" pattern). This is declared in `packages/frontend/cloudflare.config.ts` (`triggers.fetch({ pattern: "*/*", zone: "yourannex.com" })`, mirrored in `wrangler.toml`), so deploying the frontend creates it. -5. **Config** (`packages/api/cloudflare.config.ts`, mirrored in `wrangler.toml`): `CF_ZONE_ID = "397deb54a68d306201a295d1793fe84c"`, `CUSTOM_DOMAIN_CNAME_TARGET = "publish.yourannex.com"`, and `wrangler secret put CF_API_TOKEN` (token scoped to the **yourannex.com** zone with **SSL and Certificates: Edit**). +4. **Worker route**: add `*/*` → `annex-frontend` on the `yourannex.com` zone (the "Workers as your fallback origin" pattern). This is declared in `packages/frontend/cloudflare.config.ts` (`triggers.fetch({ pattern: "*/*", zone: "yourannex.com" })`), so deploying the frontend creates it. +5. **Config** (`packages/api/cloudflare.config.ts`): `CF_ZONE_ID = "397deb54a68d306201a295d1793fe84c"`, `CUSTOM_DOMAIN_CNAME_TARGET = "publish.yourannex.com"`, and `wrangler secret put CF_API_TOKEN` (token scoped to the **yourannex.com** zone with **SSL and Certificates: Edit**). 6. Customers then add: a **CNAME** `their.domain → publish.yourannex.com`, plus the **TXT** ownership + SSL-DCV records shown in Site Settings. -**Cleanup of the old shared-zone setup:** delete the stale `*/*` Worker route on the `cubityfir.st` zone (wrangler won't remove it - it only manages routes in the frontend toml, which now targets `yourannex.com`). No carve-out routes (`*.cubityfir.st/*` → None) are needed once `cubityfir.st` has no `*/*`. +**Cleanup of the old shared-zone setup:** delete the stale `*/*` Worker route on the `cubityfir.st` zone (deploys won't remove it - they only manage the routes declared in the frontend's `cloudflare.config.ts`, which now targets `yourannex.com`). No carve-out routes (`*.cubityfir.st/*` → None) are needed once `cubityfir.st` has no `*/*`. ## Local testing diff --git a/memories/Ink-Stripe.md b/memories/Ink-Stripe.md index bc69e59..898b6e4 100644 --- a/memories/Ink-Stripe.md +++ b/memories/Ink-Stripe.md @@ -100,7 +100,7 @@ The CLI prints a `whsec_...` at startup - that's what goes in `STRIPE_WEBHOOK_SE ## Production setup -- `STRIPE_INK_PRICE_ID` is a `bindings.text` var in `packages/auth/cloudflare.config.ts` (mirrored in `wrangler.toml` `[vars]`) (live price id - committed). `.dev.vars` overrides locally to the test-mode price id. +- `STRIPE_INK_PRICE_ID` is a `bindings.text` var in `packages/auth/cloudflare.config.ts` (live price id - committed). `.dev.vars` overrides locally to the test-mode price id. - `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` set via `wrangler secret put` on the auth worker. `STRIPE_SECRET_KEY` also on admin worker. - Webhook endpoint URL in Stripe Dashboard: `https://docs.cubityfir.st/stripe/webhook`. Subscribed events: `checkout.session.completed`, `customer.subscription.created/updated/deleted`, `invoice.paid`, `invoice.payment_failed`. - Customer Portal must be configured separately in **live** mode (Settings → Billing → Customer portal). Test mode and live mode have different portal configs. diff --git a/memories/Organizations.md b/memories/Organizations.md index 065ab96..cdba99f 100644 --- a/memories/Organizations.md +++ b/memories/Organizations.md @@ -33,8 +33,8 @@ site in the org. Sites don't need an org, but each belongs to at most one. organizations(id) ON DELETE SET NULL`. Indexes: `idx_org_members_user`, `idx_projects_org`. - Local apply diverged from the migrations tracker (0053 was applied manually), - so 0054 was applied to local dev via `wrangler d1 execute --file` (not - `migrations apply`). Remote/deploy uses normal migration tracking. + so 0054 was applied to local dev by executing the file directly (today: + `node scripts/d1-local.mjs main --file ...`, not `migrations apply`). Remote/deploy uses normal migration tracking. ## The access boundary - `packages/api/src/lib/access.ts` diff --git a/packages/admin/wrangler.config.ts b/packages/admin/wrangler.config.ts index 8da29cc..1f639c0 100644 --- a/packages/admin/wrangler.config.ts +++ b/packages/admin/wrangler.config.ts @@ -3,10 +3,11 @@ import { defineWranglerConfig } from "wrangler/experimental-config"; // Wrangler build settings for `cf build` / `cf deploy` (Worker config and // bindings live in cloudflare.config.ts). // -// Local dev (`pnpm dev`) still runs `wrangler dev --persist-to ../../.wrangler/state` -// against wrangler.toml: every worker must share one local state dir (the api -// worker reads the auth DB via AUTH_DB), and `cf dev` cannot set a persist -// directory yet (it always uses /.wrangler/state). +// There is no wrangler.toml. Local dev (`pnpm dev`) runs +// `wrangler dev --experimental-new-config --persist-to ../../.wrangler/state`, +// which reads this file + cloudflare.config.ts directly: every worker must share +// one local state dir (the api worker reads the auth DB via AUTH_DB), and +// `cf dev` cannot set a persist directory yet. export default defineWranglerConfig({ // Built by `pnpm build` (the admin SPA in ./frontend). assetsDirectory: "./dist", diff --git a/packages/api/wrangler.config.ts b/packages/api/wrangler.config.ts index ab8ecc4..4ce6473 100644 --- a/packages/api/wrangler.config.ts +++ b/packages/api/wrangler.config.ts @@ -3,10 +3,11 @@ import { defineWranglerConfig } from "wrangler/experimental-config"; // Wrangler build settings for `cf build` / `cf deploy` (Worker config and // bindings live in cloudflare.config.ts). // -// Local dev (`pnpm dev`) still runs `wrangler dev --persist-to ../../.wrangler/state` -// against wrangler.toml: every worker must share one local state dir (the api -// worker reads the auth DB via AUTH_DB), and `cf dev` cannot set a persist -// directory yet (it always uses /.wrangler/state). +// There is no wrangler.toml. Local dev (`pnpm dev`) runs +// `wrangler dev --experimental-new-config --persist-to ../../.wrangler/state`, +// which reads this file + cloudflare.config.ts directly: every worker must share +// one local state dir (the api worker reads the auth DB via AUTH_DB), and +// `cf dev` cannot set a persist directory yet. export default defineWranglerConfig({ // Match the previous `wrangler deploy` default (no source map upload). uploadSourceMaps: false, diff --git a/packages/auth/wrangler.config.ts b/packages/auth/wrangler.config.ts index ab8ecc4..4ce6473 100644 --- a/packages/auth/wrangler.config.ts +++ b/packages/auth/wrangler.config.ts @@ -3,10 +3,11 @@ import { defineWranglerConfig } from "wrangler/experimental-config"; // Wrangler build settings for `cf build` / `cf deploy` (Worker config and // bindings live in cloudflare.config.ts). // -// Local dev (`pnpm dev`) still runs `wrangler dev --persist-to ../../.wrangler/state` -// against wrangler.toml: every worker must share one local state dir (the api -// worker reads the auth DB via AUTH_DB), and `cf dev` cannot set a persist -// directory yet (it always uses /.wrangler/state). +// There is no wrangler.toml. Local dev (`pnpm dev`) runs +// `wrangler dev --experimental-new-config --persist-to ../../.wrangler/state`, +// which reads this file + cloudflare.config.ts directly: every worker must share +// one local state dir (the api worker reads the auth DB via AUTH_DB), and +// `cf dev` cannot set a persist directory yet. export default defineWranglerConfig({ // Match the previous `wrangler deploy` default (no source map upload). uploadSourceMaps: false, diff --git a/scripts/register-oauth-client.mjs b/scripts/register-oauth-client.mjs index cfe04f4..25ea03e 100644 --- a/scripts/register-oauth-client.mjs +++ b/scripts/register-oauth-client.mjs @@ -17,7 +17,8 @@ // // Then apply the generated file: // cd packages/auth && npx wrangler d1 execute cubedocs-auth --remote --file -// # local dev: --local --persist-to ../../.wrangler/state (instead of --remote) +// # local dev (with `pnpm dev` running), from the repo root: +// node scripts/d1-local.mjs auth --file import { webcrypto } from "node:crypto"; import { randomBytes, randomUUID } from "node:crypto";