diff --git a/AGENTS.md b/AGENTS.md index 95ef5b1..99ba578 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,23 +2,30 @@ ## Project Structure -- `src/index.ts` — Cloudflare Worker entrypoint: HTTP auth gate, REST API (`/api/mails`, `/api/mails/:id`), email handler, AI extraction, D1 writes, promotional filter. -- `src/rpcEmail.ts` — RPC-compatible `ForwardableEmailMessage` wrapper. -- `web/` — React 18 + Vite + Tailwind + shadcn/ui frontend. Built to `web/dist`, served via Cloudflare `ASSETS` binding. -- `db/schema.sql` — D1 schema: `raw_mails` (every incoming email) + `code_mails` (AI-extracted codes only). +- `src/index.ts` — `WorkerEntrypoint` shell: `fetch()` → Hono app, `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). +- `src/middleware/auth.ts` — `sessionAuth` (JWT cookie `authinbox_session`), `requireAdmin`, `apiKeyAuth` (Bearer `aik_…` keys). +- `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`. (`db/schema.sql` is the pre-migration legacy dump — do not use it.) ## Commands ```bash # Install -pnpm install # root (Worker) deps +pnpm install # root (Worker) deps; pnpm workspace includes web/ # Dev pnpm run dev # wrangler dev — local Worker on :8787 -pnpm run dev:remote # wrangler dev --remote — use live D1 -pnpm -C web run dev # Vite dev server on :5173 (proxies /api → :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 + wrangler deploy +pnpm run deploy # build:web + db:migrate:remote + wrangler deploy # Misc pnpm run test # vitest with @cloudflare/vitest-pool-workers pnpm run cf-typegen # regenerate Worker type bindings @@ -26,8 +33,8 @@ pnpm run cf-typegen # regenerate Worker type bindings Local setup: ```bash -cp wrangler.toml.example wrangler.toml # fill in database_id + secrets -pnpm wrangler d1 execute inbox-d1 --local --file=db/schema.sql +cp wrangler.toml.example wrangler.toml # fill in database_id + AI config +pnpm run db:migrate:local pnpm run build:web pnpm run dev ``` @@ -36,14 +43,22 @@ pnpm run dev - **Promotional filter runs before LLM.** `isPromotionalEmail()` checks `List-Unsubscribe`, `List-ID`, `Precedence: bulk/list`, and known ESP X-headers. 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. -- **`ASSETS` is required for the frontend.** The Worker serves the React app from the `ASSETS` binding; there is no legacy HTML fallback. +- **`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` behind `apiKeyAuth`. 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. -- **`ASSETS` binding name must be `ASSETS`** (matched in `src/index.ts` as `env.ASSETS`). -- **Base64 MIME parts** are decoded with `atob()` in `extractMailBodies()`. Cloudflare Workers runtime supports `atob`/`btoa`. +- **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. ## AI Provider Configuration -The AI layer uses a single unified function `callProvider(config: ProviderConfig, prompt)` in `src/index.ts`. All provider-specific logic is contained there — do not add new per-provider methods. +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):** @@ -53,10 +68,12 @@ The AI layer uses a single unified function `callProvider(config: ProviderConfig | `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×). @@ -73,9 +90,9 @@ Fallback is only active when all four `AI_FALLBACK_*` vars are set. Primary retr ## Security -- Never commit `wrangler.toml` with real secrets. -- `web/src/App.tsx` uses DOMPurify + sandboxed iframe for email HTML preview. Do not relax sandbox. -- Basic Auth gate is enforced at the top of `WorkerEntrypoint.fetch()` before any API route. +- 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 diff --git a/CLAUDE.md b/CLAUDE.md index 461f0ac..bb8abb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,33 +2,37 @@ ## Stack -Cloudflare Worker (TypeScript) + React 18 + Vite + Tailwind + shadcn/ui + Cloudflare D1. +Cloudflare Worker (TypeScript + Hono) + React 18 + Vite + Tailwind + shadcn/ui + Cloudflare D1 + remote MCP server (`@hono/mcp`). Package manager: **pnpm**. Never use npm commands. ## Key Commands ```bash -pnpm run dev # Worker backend :8787 -pnpm -C web run dev # React frontend :5173 (proxy /api → :8787) -pnpm run dev:remote # backend with live D1 -pnpm run deploy # build:web + wrangler deploy +pnpm run dev # Worker backend :8787 +pnpm run dev:web # React frontend :5173 (proxy /api → :8787) +pnpm run db:migrate:local # apply D1 migrations locally +pnpm run deploy # build:web + db:migrate:remote + wrangler deploy pnpm run test ``` ## Architecture Boundaries — Never Violate -1. `isPromotionalEmail()` must run **before** any LLM call in `email()` handler. +1. `isPromotionalEmail()` must run **before** any LLM call in the email handler (`src/email/handler.ts`). 2. Every incoming email → `raw_mails`. Only AI-extracted results → `code_mails`. Never skip `raw_mails`. 3. `ASSETS` is required for the frontend. There is no legacy HTML fallback. 4. DOMPurify + sandboxed iframe on all email HTML rendering. Do not relax. -5. Basic Auth gate is first in `WorkerEntrypoint.fetch()`. Do not move or bypass. -6. All AI calls go through `callProvider()`. Do not add per-provider methods alongside it. +5. Auth boundaries are middleware in `src/app.ts`: `sessionAuth` on `/api/*`, `requireAdmin` on `/api/admin/*`, `apiKeyAuth` on `/mcp`. Do not move or bypass. +6. `visibleMails()` (`src/services/mail.ts`) is the **only** mail permission layer — REST and MCP both query through it. Never add a parallel filter path. +7. Sensitive categories (`password_reset`, `account_security`) are default-deny for users; `legacy` and raw email bodies are admin-only on every interface. +8. All AI calls go through `callProvider()` (`src/services/classify.ts`). Do not add per-provider methods alongside it. +9. Schema changes = new numbered file in `migrations/`. Never edit applied migrations. -## AI Provider Env Vars +## Env Vars -Required: `AI_BASE_URL`, `AI_API_KEY`, `AI_API_FORMAT` (`openai`|`responses`|`anthropic`), `AI_MODEL`. +Required: `AI_BASE_URL`, `AI_API_KEY`, `AI_API_FORMAT` (`openai`|`responses`|`anthropic`), `AI_MODEL`, `JWT_SECRET` (Cloudflare secret, signs session JWTs). Optional fallback: `AI_FALLBACK_BASE_URL`, `AI_FALLBACK_API_KEY`, `AI_FALLBACK_API_FORMAT`, `AI_FALLBACK_MODEL` (all four or none). +Optional Bark push: `UseBark`, `barkUrl`, `barkTokens`. ## Frontend Component Rules diff --git a/README.md b/README.md index d69a719..c9881d4 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,304 @@ -# AuthInbox v2 骨架 +# Auth Inbox 📬 -Hono 重构 + 多用户/授权 + Remote MCP Server。本目录是可直接落进现有仓库的骨架,前端部分需要你自己把 Basic Auth 换成登录页(见下文)。 +[English](https://github.com/TooonyChen/AuthInbox/blob/main/README.md) | [简体中文](https://github.com/TooonyChen/AuthInbox/blob/main/README_CN.md) -## 目录结构 +**Auth Inbox** is an open-source, self-hosted email verification code platform built on [Cloudflare](https://cloudflare.com/)'s free serverless services. It automatically processes incoming emails, filters out promotional mail before hitting the AI, extracts verification codes or links, classifies each mail, and stores everything in a database. A modern React dashboard with account login lets an **admin** manage everything, while **regular users** only see the mails an admin has granted them. AI agents can read the inbox too, through a built-in **remote MCP server**. -``` -migrations/ - 0001_baseline.sql # 现有 schema, 幂等 - 0002_multi_user.sql # users / api_keys / grants / category 列 -src/ - index.ts # WorkerEntrypoint 壳: fetch → Hono, email → handler - app.ts # Hono app + 认证边界 - types.ts # Env / AppEnv / 分类枚举 - middleware/auth.ts # sessionAuth / requireAdmin / apiKeyAuth - routes/ - auth.ts # /api/auth: setup, login, logout, me - mails.ts # /api/mails: 列表 + 详情 (自动按用户过滤) - admin.ts # /api/admin: users + grants 管理 - keys.ts # /api/keys: 自助签发 MCP API key - services/ - auth.ts # PBKDF2 / JWT / API key - mail.ts # visibleMails — 唯一的权限过滤查询层 - classify.ts # LLM 提取 (prompt 加了 category) - mime.ts # MIME/encoded-word 解码、HTML 去标签、推广邮件识别 - email/ - handler.ts # email() 逻辑, 写入 category - rpcEmail.ts # 原样保留 - mcp/server.ts # /mcp: list_addresses / list_codes / get_latest_code / wait_for_code +Don't want ads and spam in your main inbox? Need a bunch of alternative addresses for signups? Want your AI agent to complete sign-up flows by reading OTPs on its own? Try this **secure**, **serverless**, **lightweight** service! + +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) + +```mermaid +flowchart LR + A([Your Inboxes]) --> B[Email Worker] + B --> C{Promotional?} + C -->|Yes| D[Save raw only] + C -->|No| E[AI Extraction + Category] + E --> F[(D1 Database)] + F --> G[React Dashboard] + F --> H([Bark iOS Push]) + F --> I[Remote MCP Server] + I --> J([AI Agents]) ``` -## 从现有仓库迁移 +--- -1. 安装依赖: +## Table of Contents 📑 + +- [Features](#features-) +- [Technologies Used](#technologies-used-) +- [Installation](#installation-) +- [Multi-User & Access Grants](#multi-user--access-grants-) +- [MCP Server (for AI Agents)](#mcp-server-for-ai-agents-) +- [Upgrading from v1](#upgrading-from-v1-) +- [License](#license-) +- [Screenshots](#screenshots-) + +--- + +## Features ✨ + +- **Promotional Filter**: Detects and skips bulk/marketing emails via headers (`List-Unsubscribe`, `Precedence: bulk`, etc.) before calling the AI — saves tokens. +- **AI Code Extraction & Classification**: Any OpenAI-compatible or Anthropic provider extracts verification codes, links, and organization names, and tags each mail with a category (`login_code`, `registration`, `password_reset`, `account_security`, `payment`, `other`). +- **Multi-User Access Control**: Admin creates users and grants them access per address pattern and per category. Sensitive categories (password resets, account security alerts) are denied to regular users by default and raw email bodies are admin-only — enforced at the SQL query layer, for REST and MCP alike. +- **Remote MCP Server**: Every user can self-issue API keys and connect AI agents (Claude Code, Cursor, …) over Streamable HTTP. Includes a `wait_for_code` tool that blocks until a fresh OTP arrives — perfect for automated sign-up flows. +- **Modern Dashboard**: React 18 + shadcn/ui interface with account login, mail list with category badges, detail panel, API key management, and an admin page for users and grants. +- **Safe HTML Preview**: Email HTML is sanitized with DOMPurify and rendered in a sandboxed iframe (admin only). +- **One-click Copy**: Verification codes and links have copy buttons with toast confirmation. +- **Real-Time Notifications**: Optionally sends Bark push notifications when new codes arrive. +- **Database**: Stores all raw emails and AI-extracted results in Cloudflare D1, with schema managed by migrations. + +--- + +## Technologies Used 🛠️ + +- **Cloudflare Workers** — Serverless runtime for email handling and API. +- **Hono** — Web framework for routing, middleware, and JWT sessions. +- **Cloudflare D1** — SQLite-compatible serverless database. +- **Cloudflare Email Routing** — Routes incoming emails to the Worker. +- **React 18 + Vite + Tailwind CSS + shadcn/ui** — Frontend dashboard. +- **Any OpenAI-compatible / Anthropic AI provider** — Configurable via env vars; Gemini, OpenAI, DeepSeek, Groq, Anthropic, and more. +- **Model Context Protocol (MCP)** — `@modelcontextprotocol/sdk` + `@hono/mcp` for the remote agent interface. +- **Bark API** — Optional iOS push notifications. +- **TypeScript** — End-to-end type safety. + +--- + +## Installation ⚙️ + +### Prerequisites + +- An API key from any supported AI provider (e.g. [Google AI Studio](https://aistudio.google.com/), OpenAI, Anthropic, DeepSeek, Groq) +- A domain bound to your [Cloudflare](https://dash.cloudflare.com/) account +- Cloudflare Account ID and API Token from [here](https://dash.cloudflare.com/profile/api-tokens) +- *(Optional)* [Bark App](https://bark.day.app/) for iOS push notifications + +--- + +### Option 1 — Deploy via GitHub Actions + +1. **Create D1 Database** + + Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → `Workers & Pages` → `D1 SQL Database` → `Create`. Name it `inbox-d1`. + + Copy the `database_id` for the next step. Tables are managed by the `migrations/` directory in this repo — the deploy workflow applies D1 migrations automatically before deploying. + +2. **Fork & Deploy** + + [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) + + In your forked repository, go to `Settings` → `Secrets and variables` → `Actions` and add: + - `CLOUDFLARE_ACCOUNT_ID` + - `CLOUDFLARE_API_TOKEN` + - `TOML` — use the [comment-free template](https://github.com/TooonyChen/AuthInbox/blob/main/wrangler.toml.example.clear), fill in your D1 `database_id` and AI config, to avoid parse errors. + + Then go to `Actions` → `Deploy Auth Inbox to Cloudflare Workers` → `Run workflow`. + + After success, go to your Worker's `Settings` → `Variables and Secrets` and add a secret named `JWT_SECRET` (a long random string). Then open your Worker URL — since the users table is empty, the login page will prompt you to create the first admin account. + + **Delete the workflow logs** afterwards to avoid leaking your config. + +3. Jump to [Set Email Forwarding](#3-set-email-forwarding-). + +--- + +### Option 2 — Deploy via CLI + +1. **Clone and install** ```bash - pnpm add hono @hono/mcp @modelcontextprotocol/sdk zod + git clone https://github.com/TooonyChen/AuthInbox.git + cd AuthInbox + pnpm install ``` -2. 设置 secret(JWT 签名用,随便一串长随机字符串)。生产环境不要把它写进 `wrangler.toml`: +2. **Create D1 database** + + ```bash + pnpm wrangler d1 create inbox-d1 + ``` + + Copy the `database_id` from the output. + +3. **Configure** + + ```bash + cp wrangler.toml.example wrangler.toml + ``` + + Edit `wrangler.toml` and fill in at least: + + ```toml + [vars] + UseBark = "false" + + # AI provider + AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" + AI_API_KEY = "your-api-key" + AI_API_FORMAT = "openai" + AI_MODEL = "gemini-2.0-flash" + + [[d1_databases]] + binding = "DB" + database_name = "inbox-d1" + database_id = "" + ``` + + **`AI_API_FORMAT`** is one of: + + | Value | Request path | Providers | + |---|---|---| + | `openai` | `/v1/chat/completions` | OpenAI, Gemini (OpenAI-compat), DeepSeek, Groq, Mistral, … | + | `responses` | `/v1/responses` | OpenAI Responses API | + | `anthropic` | `/v1/messages` | Anthropic Claude direct | + + **Common `AI_BASE_URL` values:** + ``` + OpenAI: https://api.openai.com + Gemini (OpenAI-compat): https://generativelanguage.googleapis.com/v1beta/openai + Anthropic: https://api.anthropic.com + DeepSeek: https://api.deepseek.com + Groq: https://api.groq.com/openai + ``` + + **Fallback provider (optional)**, used after the primary fails 3 retries: + ```toml + # AI_FALLBACK_BASE_URL = "https://api.openai.com" + # AI_FALLBACK_API_KEY = "fallback-api-key" + # AI_FALLBACK_API_FORMAT = "openai" + # AI_FALLBACK_MODEL = "gpt-4o-mini" + ``` + + Optional Bark config: `barkTokens`, `barkUrl`. + + Set the JWT secret (do **not** put it in `wrangler.toml` for production): ```bash pnpm exec wrangler secret put JWT_SECRET ``` -3. `FrontEndAdminID` / `FrontEndAdminPassword` 已被 users 表取代,不再需要配置。首次部署后在登录页创建第一个 admin。 - -4. 迁移数据库。`pnpm run deploy` 会自动先执行远端 D1 migrations 再部署 Worker;也可以单独执行: +4. **Build and deploy** ```bash - pnpm run db:migrate:remote + pnpm run deploy ``` -5. 部署后创建第一个 admin。登录页会自动检测 `GET /api/auth/setup`;users 表为空时会显示创建 admin 表单。也可以用 API: + This builds the frontend, applies remote D1 migrations, and deploys the Worker. - ```bash - curl -X POST https://your.domain/api/auth/setup \ - -H 'Content-Type: application/json' \ - -d '{"username":"tony","password":"一个强密码"}' - ``` + Output: `https://auth-inbox..workers.dev` -## 前端 (web/, 已完成) +--- -沿用原有主题(近黑 + 薄荷绿 #5fe0c0 + Manrope / IBM Plex Mono + 左上光晕),零新依赖,新增两个 shadcn 组件(input、label)。 +### 3. Set Email Forwarding ✉️ -- `src/api.ts`: 统一 fetch 层、类型、分类常量(与后端 MAIL_CATEGORIES 同步维护) -- `src/App.tsx`: 挂载时 GET `/api/auth/me`,401 进登录页;顶部导航按角色渲染,右上角显示当前用户和角色 -- `src/pages/LoginPage.tsx`: 登录页。会先查 GET `/api/auth/setup`,users 表为空时自动切成"创建 admin 账号"模式,建号后直接登录,不用手动 curl setup 接口 -- `src/pages/InboxPage.tsx`: 列表加了分类徽章列和发件方搜索(回车触发,走 `service` 参数)。详情页按角色分叉:admin 保留 Extracted / Raw / Rendered 三个 tab(DOMPurify + sandboxed iframe 逻辑原样保留);user 只渲染 Extracted 面板,底部一行说明原文仅 admin 可见 -- `src/pages/KeysPage.tsx`: 自助创建 / 吊销 API key,明文只展示一次(带复制按钮),右侧卡片给出 MCP server URL 和 Claude Code 接入命令(自动取当前域名) -- `src/pages/AdminPage.tsx`: 左卡片用户管理(建号、删号,不能删自己),右卡片授权管理(选用户、填 GLOB pattern、点选分类)。选中敏感分类时出现红色确认框,对应后端的 `allow_sensitive`,不勾的话服务端会把敏感分类从 grant 里剔除 -- 分类徽章配色:login_code 用主题薄荷绿,registration 天蓝,payment 琥珀,敏感类暗红,legacy 虚线灰 +Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → `Websites` → `` → `Email` → `Email Routing` → `Routing Rules`. -旧的 `src/index.html` 服务端兜底页在这版里去掉了,因为它无法感知用户身份。如果你想保留 no-JS 兜底,可以在 `app.ts` 的 SPA fallback 前加一个 session 校验的服务端渲染路由。 +**Catch-all address** (forward all mail to the Worker): +![image](https://github.com/user-attachments/assets/53e5a939-6b03-4ca6-826a-7a5f02f361ac) -## 授权模型 +**Custom addresses** (forward specific addresses): +![image](https://github.com/user-attachments/assets/b0d0ab94-c2ad-4870-ac08-d53e64b2c880) -- grant = (user, 地址 pattern, 允许的分类, 是否放行敏感类) -- pattern 用 SQLite GLOB:`netflix@mail.tony.dev` 精确,`*@mail.tony.dev` 通配 -- 分类由 LLM 在提取时打标:`login_code` / `registration` / `password_reset` / `account_security` / `payment` / `other` -- `password_reset` 和 `account_security` 是敏感类:grant 里就算写了,`allow_sensitive = 0` 时服务层也会剔除 -- 迁移前的历史邮件 category 为 `legacy`,永远只有 admin 可见 -- 普通用户在任何接口(含 MCP)都拿不到邮件原文 raw,只能看到提取后的结构化结果 +### 4. Done! 🎉 -给 user 开一个典型 grant: +Open your Worker URL. On first visit (empty users table) the login page turns into a "create admin account" form — create the first admin and you're in. -```bash -curl -X POST https://your.domain/api/admin/grants \ - -H 'Content-Type: application/json' -b 'authinbox_session=...' \ - -d '{ - "userId": 2, - "addressPattern": "*@mail.tony.dev", - "allowedCategories": ["login_code", "registration"] - }' -``` +--- -这样 user 2 能看到发到该域名任何地址的 Netflix 登录验证码,但看不到 Netflix 改密码邮件。 +## Multi-User & Access Grants 🔐 -## MCP 接入 +Two roles: **admin** and **user**. -1. 用户登录 web 后 POST `/api/keys` 拿到 `aik_xxx`(只显示一次)。 -2. Claude Code: +- **Admin** sees everything: all mails, raw bodies, rendered HTML, user management, grant management. +- **User** only sees mails matched by grants an admin created for them, and never sees raw email bodies — only the AI-extracted results. + +A **grant** = (user, address pattern, allowed categories, allow-sensitive flag): + +- Address patterns use SQLite GLOB: `netflix@mail.example.com` for exact match, `*@mail.example.com` for a whole domain. +- Every mail is classified at ingest time: `login_code` / `registration` / `password_reset` / `account_security` / `payment` / `other`. +- `password_reset` and `account_security` are **sensitive**: even if listed in a grant, they are stripped unless the grant explicitly sets `allow_sensitive`. Fail-safe by design — a misclassified mail means a user sees one less mail, never one more. +- Mails ingested before v2 are categorized `legacy` and are always admin-only. + +So a user can be allowed to read the Netflix **login code** sent to a shared address, while the Netflix **password reset** mail on the same address stays admin-only. Everything is enforced in a single SQL-level query function shared by the web API and the MCP server. + +Manage users and grants in the dashboard's **Users & Access** page (admin only). + +--- + +## MCP Server (for AI Agents) 🤖 + +Auth Inbox exposes a remote MCP server at `https:///mcp` (Streamable HTTP). + +1. Log in to the dashboard → **API Keys** → create a key (`aik_…`, shown only once). Keys inherit your role and grants. +2. Connect from Claude Code: ```bash claude mcp add --transport http authinbox https://your.domain/mcp \ --header "Authorization: Bearer aik_xxx" ``` -3. Tools: - - `list_addresses`: 该 key 对应用户可见的收件地址 - - `list_codes` / `get_latest_code`: 查验证码 - - `wait_for_code`: 阻塞等新码(最长 55s,只返回调用之后到达的邮件),agent 注册流程的关键工具 +3. Available tools: -claude.ai 的远程连接器要求 OAuth,这版先不做;以后要接的话在 `/mcp` 前面套 Cloudflare 的 `workers-oauth-provider` 即可,tool 层不用动。 + | Tool | Purpose | + |---|---| + | `list_addresses` | List inbox addresses the key's user may read | + | `list_codes` | List recent codes/links, filterable by address or service | + | `get_latest_code` | Get the single most recent code for an address/service | + | `wait_for_code` | Block (up to 55s) until a **new** code arrives — for automated sign-up/login flows | -## 验证清单 +All tools go through the same permission filter as the web API, so an agent holding a user's key can never read sensitive or raw content. -- [ ] 旧邮件进件流程不变:发一封测试邮件,raw_mails 和 code_mails 都有记录,category 有值 -- [ ] 推广邮件仍然在 LLM 之前被拦截 -- [ ] user 登录后 `/api/mails` 只返回 grant 范围内、非敏感分类的邮件 -- [ ] user 请求 `/api/mails/:id` 越权时返回 404 -- [ ] user 的 MCP key 调 `list_codes` 结果与 web 端一致 -- [ ] `wait_for_code` 期间发送邮件,能在轮询里被捞到 -- [ ] Bark 推送不变 +> claude.ai remote connectors require OAuth, which is not implemented yet — see TODO. + +--- + +## Upgrading from v1 ⬆️ + +v2 is a breaking change: + +1. **Basic Auth is gone.** `FrontEndAdminID` / `FrontEndAdminPassword` in `wrangler.toml` are no longer used — remove them. Accounts live in the D1 `users` table. +2. **Set a `JWT_SECRET`** secret: `pnpm exec wrangler secret put JWT_SECRET`. +3. **Run migrations**: `pnpm run db:migrate:remote` (also runs automatically in `pnpm run deploy` and the GitHub Actions workflow). Existing data is preserved; old mails are marked category `legacy` (admin-only). +4. **Create the first admin** on the login page after deploying (or `POST /api/auth/setup` — only works while the users table is empty). + +--- + +## License 📜 + +[MIT License](LICENSE) + +--- + +## Screenshots 📸 + +![Mail list with extracted codes](img/Group%201.png) + +![Mail detail with verification code](img/Group%202.png) + +![Rendered HTML email preview](img/Group%203.png) + +--- + +## Acknowledgements 🙏 + +- **Cloudflare Workers** for the powerful serverless platform. +- **Hono** and the **Model Context Protocol** teams. +- **shadcn/ui** for the component library. +- **Bark** for real-time push notifications. +- **The open-source community** for inspiration and support. + +--- + +## TODO 📝 + +- [x] GitHub Actions auto-deploy +- [x] Fallback AI provider support +- [x] React dashboard (shadcn/ui) +- [x] Promotional filter (before any AI call) +- [x] Raw email view + sandboxed HTML preview +- [x] Multi-user support (admin/user roles + per-address, per-category grants) +- [x] Remote MCP server for AI agents +- [ ] OAuth on `/mcp` (for claude.ai remote connectors) +- [ ] Regex extraction (no-AI option) +- [ ] More notification channels (Slack, webhook, …) +- [ ] Email sending diff --git a/README_CN.md b/README_CN.md index b30a7bc..ceaea86 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,9 +2,9 @@ [English](https://github.com/TooonyChen/AuthInbox/blob/main/README.md) | [简体中文](https://github.com/TooonyChen/AuthInbox/blob/main/README_CN.md) -**Auth Inbox** 是一个开源的自建验证码接码平台,基于 [Cloudflare](https://cloudflare.com/) 的免费无服务器服务。它会自动处理收到的邮件,在调用 AI 之前过滤掉广告邮件,提取验证码或链接并存入数据库。现代化的 React 仪表盘让管理员可以查看提取的验证码、查看原始邮件内容、渲染 HTML 邮件预览,全程受 Basic Auth 保护。 +**Auth Inbox** 是一个开源的自建验证码接码平台,基于 [Cloudflare](https://cloudflare.com/) 的免费无服务器服务。它会自动处理收到的邮件,在调用 AI 之前过滤掉广告邮件,提取验证码或链接、自动分类并存入数据库。现代化的 React 仪表盘支持账号登录:**admin** 管理一切,**普通用户**只能看到 admin 授权范围内的邮件。内置**远程 MCP Server**,AI agent 也能直接读取收件箱。 -不想在主邮箱中收到广告和垃圾邮件?需要多个备用邮箱用于注册各类服务?试试这个**安全**、**无服务器**、**轻量**的解决方案吧! +不想在主邮箱中收到广告和垃圾邮件?需要多个备用邮箱用于注册各类服务?想让 AI agent 自己读验证码完成注册流程?试试这个**安全**、**无服务器**、**轻量**的解决方案吧! [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/TooonyChen/AuthInbox) @@ -12,11 +12,13 @@ flowchart LR A([你的邮箱]) --> B[Email Worker] B --> C{广告邮件?} - C -->|是| D[丢弃] - C -->|否| E[AI 提取验证码] + C -->|是| D[仅存原始邮件] + C -->|否| E[AI 提取 + 分类] E --> F[(D1 数据库)] F --> G[React 管理面板] F --> H([Bark iOS 推送]) + F --> I[远程 MCP Server] + I --> J([AI Agents]) ``` --- @@ -26,6 +28,9 @@ flowchart LR - [功能](#功能-) - [使用的技术](#使用的技术-) - [安装](#安装-) +- [多用户与授权](#多用户与授权-) +- [MCP Server(AI Agent 接入)](#mcp-serverai-agent-接入-) +- [从 v1 升级](#从-v1-升级-) - [许可证](#许可证-) - [截图](#截图-) @@ -34,22 +39,26 @@ flowchart LR ## 功能 ✨ - **广告过滤**:通过邮件头(`List-Unsubscribe`、`Precedence: bulk` 等)识别并跳过营销邮件,不调用 AI,节省 token。 -- **AI 验证码提取**:使用 Google Gemini(OpenAI 作为备用)提取验证码、链接和组织名称。 -- **现代化仪表盘**:React 18 + shadcn/ui 界面,包含邮件列表、详情面板和三个 Tab —— 提取内容、原始邮件、HTML 渲染预览。 -- **安全 HTML 预览**:邮件 HTML 经 DOMPurify 净化后在沙箱 iframe 中渲染。 +- **AI 验证码提取与分类**:支持任意 OpenAI 兼容 / Anthropic 提供商,提取验证码、链接和组织名称,并给每封邮件打上分类标签(`login_code` / `registration` / `password_reset` / `account_security` / `payment` / `other`)。 +- **多用户权限管理**:admin 创建用户,按地址 pattern + 分类授权。敏感分类(改密码、账户安全警告)对普通用户默认拒绝,邮件原文仅 admin 可见 —— 权限在 SQL 查询层强制执行,REST 和 MCP 共用同一套过滤。 +- **远程 MCP Server**:每个用户可自助签发 API key,让 AI agent(Claude Code、Cursor 等)通过 Streamable HTTP 接入。`wait_for_code` 工具可阻塞等待新验证码送达,适合自动注册流程。 +- **现代化仪表盘**:React 18 + shadcn/ui 界面,账号登录、带分类徽章的邮件列表、详情面板、API key 管理,以及 admin 专属的用户/授权管理页。 +- **安全 HTML 预览**:邮件 HTML 经 DOMPurify 净化后在沙箱 iframe 中渲染(仅 admin)。 - **一键复制**:验证码和链接均有复制按钮,复制后有 Toast 提示。 - **实时通知**:可选接入 Bark,收到新验证码时推送到 iOS 设备。 -- **数据库存储**:所有原始邮件和 AI 提取结果均存入 Cloudflare D1。 +- **数据库存储**:所有原始邮件和 AI 提取结果均存入 Cloudflare D1,schema 由 migrations 管理。 --- ## 使用的技术 🛠️ - **Cloudflare Workers** — 处理邮件和 API 的无服务器运行时。 +- **Hono** — Web 框架,负责路由、中间件和 JWT 会话。 - **Cloudflare D1** — 兼容 SQLite 的无服务器数据库。 - **Cloudflare Email Routing** — 将收到的邮件路由到 Worker。 - **React 18 + Vite + Tailwind CSS + shadcn/ui** — 前端仪表盘。 - **任意 OpenAI 兼容 / Anthropic AI 提供商** — 通过环境变量配置,支持 Gemini、OpenAI、DeepSeek、Groq、Anthropic 等。 +- **Model Context Protocol (MCP)** — `@modelcontextprotocol/sdk` + `@hono/mcp` 提供远程 agent 接口。 - **Bark API** — iOS 实时推送通知(可选)。 - **TypeScript** — 端到端类型安全。 @@ -59,7 +68,7 @@ flowchart LR ### 先决条件 -- 一个 [Google AI Studio API Key](https://aistudio.google.com/) +- 任意受支持 AI 提供商的 API Key(如 [Google AI Studio](https://aistudio.google.com/)、OpenAI、Anthropic、DeepSeek、Groq) - 在 [Cloudflare](https://dash.cloudflare.com/) 账户上绑定一个域名 - Cloudflare 账户 ID 和 API Token,可在 [此处](https://dash.cloudflare.com/profile/api-tokens) 获取 - *(可选)* [Bark App](https://bark.day.app/),用于 iOS 推送通知 @@ -194,7 +203,65 @@ flowchart LR ### 4. 完成!🎉 -访问你的 Worker URL,用你设置的账号密码登录,即可开始接收验证邮件。 +访问你的 Worker URL。首次访问(users 表为空)时,登录页会自动变成"创建 admin 账号"表单,创建第一个 admin 后即可开始接收验证邮件。 + +--- + +## 多用户与授权 🔐 + +两种角色:**admin** 和 **user**。 + +- **admin** 可以看到一切:所有邮件、原始邮件、HTML 渲染、用户管理、授权管理。 +- **user** 只能看到 admin 通过 grant 授权范围内的邮件,且永远看不到邮件原文,只能看到 AI 提取后的结构化结果。 + +一条 **grant** = (用户,地址 pattern,允许的分类,是否放行敏感类): + +- 地址 pattern 使用 SQLite GLOB:`netflix@mail.example.com` 精确匹配,`*@mail.example.com` 匹配整个域名。 +- 每封邮件在进件时由 AI 分类:`login_code` / `registration` / `password_reset` / `account_security` / `payment` / `other`。 +- `password_reset` 和 `account_security` 是**敏感分类**:即使写进了 grant,只要没有显式勾选 `allow_sensitive` 就会被服务层剔除。这个设计是故障安全的 —— 分类错误的代价只是"用户少看到一封该看的邮件",而不是"看到了不该看的重置链接"。 +- v2 之前的历史邮件分类为 `legacy`,永远只有 admin 可见。 + +因此可以做到:user 能看到发到共享地址的 Netflix **登录验证码**,但同一地址收到的 Netflix **改密码邮件**只有 admin 可见。所有过滤由 web API 和 MCP Server 共用的同一个 SQL 查询函数强制执行。 + +用户和授权在仪表盘的 **Users & Access** 页面管理(仅 admin 可见)。 + +--- + +## MCP Server(AI Agent 接入)🤖 + +Auth Inbox 在 `https://<你的 Worker 域名>/mcp` 暴露一个远程 MCP Server(Streamable HTTP)。 + +1. 登录仪表盘 → **API Keys** → 创建 key(`aik_…`,只显示一次)。key 继承创建者的角色和授权。 +2. 在 Claude Code 中接入: + + ```bash + claude mcp add --transport http authinbox https://your.domain/mcp \ + --header "Authorization: Bearer aik_xxx" + ``` + +3. 可用工具: + + | 工具 | 用途 | + |---|---| + | `list_addresses` | 列出该 key 对应用户可读的收件地址 | + | `list_codes` | 列出最近的验证码/链接,可按地址或服务筛选 | + | `get_latest_code` | 获取某地址/服务最新的一条验证码 | + | `wait_for_code` | 阻塞等待(最长 55 秒)**新**验证码送达 —— 自动注册/登录流程的关键工具 | + +所有工具走与 web API 相同的权限过滤,拿着 user key 的 agent 永远读不到敏感分类和邮件原文。 + +> claude.ai 的远程连接器要求 OAuth,目前尚未实现 —— 见 TODO。 + +--- + +## 从 v1 升级 ⬆️ + +v2 是破坏性变更: + +1. **Basic Auth 已移除。**`wrangler.toml` 里的 `FrontEndAdminID` / `FrontEndAdminPassword` 不再使用,可以删除。账号存储在 D1 的 `users` 表中。 +2. **设置 `JWT_SECRET`** secret:`pnpm exec wrangler secret put JWT_SECRET`。 +3. **执行迁移**:`pnpm run db:migrate:remote`(`pnpm run deploy` 和 GitHub Actions workflow 也会自动执行)。现有数据保留;旧邮件会被标记为 `legacy` 分类(仅 admin 可见)。 +4. **创建第一个 admin**:部署后在登录页创建(或调用 `POST /api/auth/setup`,仅在 users 表为空时可用)。 --- @@ -217,9 +284,9 @@ flowchart LR ## 鸣谢 🙏 - **Cloudflare Workers** 提供强大的无服务器平台。 -- **Google Gemini AI** 提供智能邮件内容提取能力。 -- **Bark** 提供实时推送通知。 +- **Hono** 和 **Model Context Protocol** 团队。 - **shadcn/ui** 提供组件库。 +- **Bark** 提供实时推送通知。 - **开源社区** 提供灵感与支持。 --- @@ -227,11 +294,13 @@ flowchart LR ## TODO 📝 - [x] GitHub Actions 自动部署 -- [x] OpenAI 备用模型支持 +- [x] 备用 AI 模型支持 - [x] React 仪表盘(shadcn/ui) - [x] 广告邮件过滤(调用 AI 前拦截) - [x] 原始邮件查看 + 沙箱 HTML 预览 +- [x] 多用户支持(admin/user 角色 + 按地址、按分类授权) +- [x] 远程 MCP Server(AI Agent 接入) +- [ ] `/mcp` 支持 OAuth(接入 claude.ai 远程连接器) - [ ] 正则表达式提取(无 AI 选项) -- [ ] 多用户支持 - [ ] 更多通知方式(Slack、Webhook 等) - [ ] 发送邮件功能 diff --git a/package.json b/package.json index 947f076..91d52a4 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,6 @@ "wrangler": "^4.73.0" }, "packageManager": "pnpm@10.32.1", - "description": "Hono 重构 + 多用户/授权 + Remote MCP Server。本目录是可直接落进现有仓库的骨架,前端部分需要你自己把 Basic Auth 换成登录页(见下文)。", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC" + "description": "Self-hosted email verification code platform on Cloudflare Workers — Hono, multi-user grants, remote MCP server for AI agents.", + "license": "MIT" } diff --git a/wrangler.toml.example b/wrangler.toml.example index 4a9396c..68f4a23 100644 --- a/wrangler.toml.example +++ b/wrangler.toml.example @@ -86,3 +86,8 @@ database_id = "********-****-****-****-************" # database_id: String # replace with your database id # 请你替换为你的数据库 ID + +# --- Required secret (not a [vars] entry) --- +# JWT_SECRET signs login session tokens. Set it as a Cloudflare Secret after deploy: +# pnpm exec wrangler secret put JWT_SECRET +# JWT_SECRET 用于签名登录会话,请在部署后通过 Cloudflare Secret 设置,不要写进 [vars]。 diff --git a/wrangler.toml.example.clear b/wrangler.toml.example.clear index 60f7721..02e1251 100644 --- a/wrangler.toml.example.clear +++ b/wrangler.toml.example.clear @@ -27,3 +27,7 @@ AI_MODEL = "gemini-2.0-flash" binding = "DB" database_name = "inbox-d1" database_id = "********-****-****-****-************" + +# Required secret (not a [vars] entry): set JWT_SECRET as a Cloudflare Secret after deploy. +# pnpm exec wrangler secret put JWT_SECRET +# 必需的 secret(不要写进 [vars]):部署后设置 JWT_SECRET。