Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .claude/wf-auth-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/*,',
Expand Down
20 changes: 10 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -122,13 +122,14 @@ Use Conventional Commits with a type **and** scope: `<type>(<scope>): <imperativ

## CLI Notes

- All wrangler commands must be prefixed with `npx` (e.g. `npx wrangler d1 execute ...`)
- All wrangler commands must be prefixed with `npx` (e.g. `npx wrangler secret put ...`)

## Worker Config (cf)

Each worker (`packages/{api,auth,admin,frontend}`) is configured by **`cloudflare.config.ts`** (Worker settings, bindings, vars, routes/domains, crons, DO `exports`) plus **`wrangler.config.ts`** (build settings: assets dir, source maps). That is what **`cf build` / `cf deploy`** and the Workers Builds deploy triggers read. Each package pins `cf` and `wrangler` (the Wrangler bundler must be installed per package, not hoisted).
Each worker (`packages/{api,auth,admin,frontend}`) is configured by **`cloudflare.config.ts`** (Worker settings, bindings, vars, routes/domains, crons, DO `exports`) plus **`wrangler.config.ts`** (build settings: assets dir, source maps). **These are the only Worker config files - there is no `wrangler.toml`/`wrangler.json`; don't add one.** They are what **`cf build` / `cf deploy`**, the Workers Builds deploy triggers, and local dev read, so a binding/var/route change goes in `cloudflare.config.ts` only. Each package pins `cf` and `wrangler` (the Wrangler bundler must be installed per package, not hoisted).

- **`wrangler.toml` is retained and must be kept in sync** - `pnpm dev` still runs `wrangler dev --persist-to ../../.wrangler/state` (`cf dev` can't share one persist dir across workers yet, and the api reads auth's DB via `AUTH_DB`), and the e2e suites/ops scripts use `npx wrangler d1 execute <db> --local`. **Any binding/var/route change goes in both files.**
- **Local dev** (`pnpm dev`): api/auth/admin run `wrangler dev --experimental-new-config --port <n> --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 <main|auth> "<sql>"` (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 <db> --remote`, `npx wrangler r2 object ... --remote`) work without a config file.
- D1 migrations: `pnpm migrate` / `node scripts/migrate.mjs <local|remote>` run `cf d1 migrations apply <DATABASE_ID>` (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`.
Expand All @@ -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 <id> --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 = '<id>'"
```

## Parallel Worktrees (dev review)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <main|auth> "<sql>"` while `pnpm dev` is running. All wrangler invocations go through `npx wrangler`.

## Packages

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
7 changes: 4 additions & 3 deletions docs/oauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> for both DBs
# local dev: pnpm migrations:local
```

4. **Deploy** the three workers that gained code (new tables aren't read by
Expand Down Expand Up @@ -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 <printed path>
# local dev (with `pnpm dev` running), from the repo root:
node scripts/d1-local.mjs auth --file <printed path>
```

Either way, put `client_id` + `client_secret` (+ the discovery URL) into the
Expand Down
21 changes: 21 additions & 0 deletions e2e/d1Local.ts
Original file line number Diff line number Diff line change
@@ -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 <db> --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);
}
}
28 changes: 8 additions & 20 deletions e2e/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -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 %';",
);
}
16 changes: 6 additions & 10 deletions e2e/tests/collab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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 });
});
Expand Down
11 changes: 3 additions & 8 deletions e2e/tests/ink-features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`;
Expand Down Expand Up @@ -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" });
Expand Down
9 changes: 2 additions & 7 deletions e2e/tests/profile-card.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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" });
Expand Down
2 changes: 1 addition & 1 deletion memories/Api-Keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<id>`, 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`
Expand Down
5 changes: 3 additions & 2 deletions memories/Collab.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<projectId>';"
node scripts/d1-local.mjs main "UPDATE projects SET features = features | 4 WHERE id = '<projectId>';"
```
Loading
Loading