Files

106 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Repository Guidelines
## Project Structure
- `src/index.ts` — `WorkerEntrypoint` shell: `fetch()` → `OAuthProvider` (`@cloudflare/workers-oauth-provider`; implements `/oauth/token`, `/oauth/register`, `/.well-known/*`, stores tokens in `OAUTH_KV`) → Hono app. `Bearer aik_` requests to `/mcp` bypass the provider straight to Hono, and the provider is skipped entirely when the optional `OAUTH_KV` binding is absent (OAuth disabled). `email()` → email handler, `rpcEmail()` RPC method.
- `src/app.ts` — Hono app: route mounting, auth middleware boundaries, SPA fallback via `ASSETS`.
- `src/routes/` — REST API: `auth.ts` (`/api/auth`: setup/login/logout/me), `mails.ts` (`/api/mails`), `admin.ts` (`/api/admin`: users + grants, admin only), `keys.ts` (`/api/keys`: self-service API keys), `oauth.ts` (`/oauth/authorize`: OAuth consent page for claude.ai remote connectors).
- `src/middleware/auth.ts` — `sessionAuth` (JWT cookie `authinbox_session`), `requireAdmin`, `apiKeyAuth` (Bearer `aik_…` keys), `oauthPropsAuth` (OAuth token props → D1 user lookup).
- `src/services/` — `mail.ts` (`visibleMails` — the single permission-filtered query layer), `auth.ts` (PBKDF2 / JWT / API keys via WebCrypto), `classify.ts` (`callProvider` LLM extraction + category), `mime.ts` (MIME decoding, promotional filter).
- `src/email/handler.ts` — incoming email pipeline; `src/email/rpcEmail.ts` — RPC-compatible `ForwardableEmailMessage` wrapper.
- `src/mcp/server.ts` — remote MCP server at `/mcp` (`@hono/mcp` Streamable HTTP): `list_addresses`, `list_codes`, `get_latest_code`, `wait_for_code`.
- `web/` — React 18 + Vite + Tailwind + shadcn/ui frontend. Built to `web/dist`, served via Cloudflare `ASSETS` binding. Pages: Login (doubles as first-admin setup), Inbox, API Keys, Users & Access (admin).
- `migrations/` — D1 schema, applied with `wrangler d1 migrations apply`. Tables: `raw_mails`, `code_mails` (+ `category`), `users`, `api_keys`, `grants`.
- `test/` — `*.test.ts` unit tests (workers pool). `test/mail/*.mail.ts` real-mail regression suite (node env, `vitest.mail.config.mts`): `pipeline.mail.ts` asserts promo-filter verdicts and LLM-input content deterministically; `classify.mail.ts` runs the live LLM eval (`EVAL_LLM=1`). Fixtures come from your own D1 via `scripts/pull-fixtures.mjs` into gitignored `test/fixtures/*.eml`; expectations live in the committed `test/fixtures/manifest.json`.
## Commands
```bash
# Install
pnpm install # root (Worker) deps; pnpm workspace includes web/
# Dev
pnpm run dev # wrangler dev — local Worker on :8787
pnpm run dev:web # Vite dev server on :5173 (proxies /api → :8787)
# Database
pnpm run db:migrate:local # apply D1 migrations locally
pnpm run db:migrate:remote # apply D1 migrations to live D1
# Build & deploy
pnpm run build:web # vite build → web/dist
pnpm run deploy # build:web + db:migrate:remote + wrangler deploy
# Misc
pnpm run test # unit tests (vitest with @cloudflare/vitest-pool-workers)
pnpm run fixtures:pull # pull real-mail fixtures from remote D1 into test/fixtures/*.eml (gitignored)
pnpm run test:mail # deterministic pipeline regression on real-mail fixtures (no LLM, free)
pnpm run test:eval # LLM classification eval on fixtures (real provider calls; EVAL_RUNS=n, default 5)
pnpm run cf-typegen # regenerate Worker type bindings
```
Local setup:
```bash
cp wrangler.toml.example wrangler.toml # fill in database_id + AI config (+ optional OAUTH_KV id for OAuth)
echo 'JWT_SECRET=any-local-dev-secret' > .dev.vars # local session signing (gitignored)
pnpm run db:migrate:local
pnpm run build:web
pnpm run dev
```
## Architecture Constraints
- **Promotional filter runs before LLM.** `isPromotionalEmail()` treats `List-ID`/`List-Post`, `Precedence: bulk/list`, and known ESP campaign X-headers as unconditional bulk signals. `List-Unsubscribe` alone is a weak signal: transactional-looking subjects (code/verify/OTP/sign-in/reset/验证码 …) are exempted, because Gmail/Yahoo bulk-sender rules force senders like Netflix/SES to add it to verification mail too. If matched, raw email is still saved to `raw_mails` but LLM is skipped.
- **Two DB tables, strict separation.** Every email → `raw_mails`. Only emails with extracted codes/links → `code_mails`. Never skip `raw_mails` insert.
- **`visibleMails()` in `src/services/mail.ts` is the only permission layer.** REST routes and MCP tools must query mails through it — never add a second filter path. Grants are enforced in SQL (GLOB address pattern + category allowlist).
- **Sensitive categories are default-deny.** `password_reset`, `account_security` require `allow_sensitive = 1` on the grant; `legacy` (pre-v2 mails) is always admin-only. Raw email bodies (`raw`/`textBody`/`htmlBody`) are admin-only on every interface.
- **Auth boundaries live in middleware.** `/api/*` behind `sessionAuth` (except `/api/auth` public endpoints), `/api/admin/*` additionally behind `requireAdmin`. `/mcp` accepts two credentials: `aik_` keys behind `apiKeyAuth` (routed in `src/index.ts` before the provider) and OAuth access tokens validated by `OAuthProvider` then resolved by `oauthPropsAuth`. Static assets are intentionally public; data only flows through authed APIs.
- **`ASSETS` is required for the frontend.** The Worker serves the React app from the `ASSETS` binding (name must be `ASSETS`); there is no server-rendered fallback.
- **`web/dist` is gitignored.** Built at deploy time via `pnpm run deploy`. No need to commit build artifacts.
- **Schema changes go through `migrations/`** as new numbered files; never edit applied migrations.
## Auth Model
- Web: username/password → PBKDF2-SHA256 (100k iterations, WebCrypto) → JWT (HS256, 7-day exp) in HttpOnly cookie `authinbox_session`. Signing key is the `JWT_SECRET` Cloudflare secret.
- First admin: `POST /api/auth/setup` works only while the `users` table is empty (the login page automates this).
- MCP / programmatic: `Authorization: Bearer aik_…` API keys; only the SHA-256 hash is stored, plaintext shown once at creation. Keys inherit the owner's role and grants.
- MCP / OAuth (claude.ai remote connectors, **optional feature**): enabled only when the `OAUTH_KV` KV namespace is bound (binding name is mandated by the library; without it `/oauth/*` returns 404 and `/mcp` accepts only `aik_` keys). OAuth 2.1 + PKCE + dynamic client registration via `@cloudflare/workers-oauth-provider`; tokens/grants/clients live in `OAUTH_KV`. The consent page at `/oauth/authorize` reuses the session cookie — unauthenticated visitors are bounced through the SPA login with `returnTo`. Grant props store only `userId`; `oauthPropsAuth` re-fetches the user from D1 on every request, so deleting a user instantly revokes OAuth access.
## AI Provider Configuration
The AI layer uses a single unified function `callProvider(config: ProviderConfig, prompt)` in `src/services/classify.ts`. All provider-specific logic is contained there — do not add new per-provider methods. The extraction prompt also assigns a `category` (`login_code` | `registration` | `password_reset` | `account_security` | `payment` | `other`); invalid output coerces to `other`.
**Env vars (set in `wrangler.toml` `[vars]` or as Cloudflare Secrets):**
| Variable | Required | Description |
|---|---|---|
| `AI_BASE_URL` | ✅ | Provider base URL, no trailing slash |
| `AI_API_KEY` | ✅ | API key (use Secret in production) |
| `AI_API_FORMAT` | ✅ | `openai` \| `responses` \| `anthropic` |
| `AI_MODEL` | ✅ | Model ID |
| `JWT_SECRET` | ✅ (Secret) | Session JWT signing key |
| `AI_FALLBACK_BASE_URL` | optional | Fallback provider base URL |
| `AI_FALLBACK_API_KEY` | optional | Fallback API key |
| `AI_FALLBACK_API_FORMAT` | optional | Fallback format |
| `AI_FALLBACK_MODEL` | optional | Fallback model ID |
| `UseBark` / `barkUrl` / `barkTokens` | optional | Bark iOS push |
Fallback is only active when all four `AI_FALLBACK_*` vars are set. Primary retries 3× before fallback is attempted (1×).
**Format → endpoint mapping:**
- `openai` → `POST /v1/chat/completions` (OpenAI, Gemini OpenAI-compat, DeepSeek, Groq, …)
- `responses` → `POST /v1/responses` (OpenAI Responses API)
- `anthropic` → `POST /v1/messages` (Anthropic Claude direct; sends `anthropic-version: 2023-06-01`, `max_tokens: 1024`)
## Coding Style
- Tabs, LF, UTF-8, no trailing whitespace (see `.editorconfig`).
- Prettier: single quotes, semicolons, print width 140.
- `PascalCase` classes/types, `camelCase` functions/variables, `UPPER_SNAKE_CASE` constants.
## Security
- Never commit `wrangler.toml` with real secrets; `JWT_SECRET` and `AI_API_KEY` belong in Cloudflare Secrets.
- Email HTML preview uses DOMPurify + sandboxed iframe. Do not relax the sandbox.
- Do not log or return API key plaintext after creation; do not expose `raw` bodies to non-admin roles.
## Commit Style
Conventional Commits: `feat:`, `fix:`, `docs:`, `chore:`. Keep commits atomic.