Files

8.9 KiB
Raw Permalink Blame History

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

# 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:

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.