From cc7b31d280f492b87b0679a993f4aa978df058bc Mon Sep 17 00:00:00 2001 From: otscup Date: Fri, 16 Oct 2026 19:01:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E8=87=AA=E6=89=98=E7=AE=A1?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E9=80=82=E9=85=8D(auth.otscup.com)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 接入 Resend 邮件通道(EMAIL_PROVIDER=resend,RESEND_API_KEY) - Argon2 迭代 3->2,适配 Workers Free 10ms CPU 上限(改密双哈希不超限) - wrangler 配置:主账号资源(auth.otscup.com 域名、xid-db/kv/r2/queues) - console 经 core service binding 转发,CANONICAL_HOST 改为 auth.otscup.com - 移除 Analytics Engine binding(账号未启用)与超限 cron triggers - .gitignore 加入 .xid-secrets Signed-off-by: otscup --- .gitignore | 1 + apps/console/worker/index.ts | 2 +- apps/console/wrangler.jsonc | 9 +-- apps/server/worker/auth/password.ts | 2 +- apps/server/worker/env.d.ts | 2 + apps/server/worker/queues/email.ts | 38 ++++++++++++ apps/server/wrangler.jsonc | 22 ++----- apps/site/wrangler.jsonc | 96 +---------------------------- packages/types/src/env.ts | 2 +- 9 files changed, 52 insertions(+), 122 deletions(-) diff --git a/.gitignore b/.gitignore index 1774a6b8..6dd14945 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ id_ed25519* __pycache__/ *.pyc .phpunit.result.cache +.xid-secrets diff --git a/apps/console/worker/index.ts b/apps/console/worker/index.ts index eaafe85f..667d2f3b 100644 --- a/apps/console/worker/index.ts +++ b/apps/console/worker/index.ts @@ -4,7 +4,7 @@ export type ConsoleEnv = { } } -const CANONICAL_HOST = 'xid.dev' +const CANONICAL_HOST = 'auth.otscup.com' const LOCAL_DEV_HOSTS = new Set(['127.0.0.1', 'localhost']) const CONSOLE_ROOT = '/console' const CONSOLE_SHELL = '/console/' diff --git a/apps/console/wrangler.jsonc b/apps/console/wrangler.jsonc index 0cc0f44e..f710e143 100644 --- a/apps/console/wrangler.jsonc +++ b/apps/console/wrangler.jsonc @@ -1,15 +1,10 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "xid-console", - "account_id": "86e4d320a5d69fb54f9721fb219a4902", + "account_id": "f475b3dcf291c33ae82ab0b595f81958", "main": "worker/index.ts", "preview_urls": false, - "routes": [ - { "pattern": "xid.dev/console", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/console/*", "zone_name": "xid.dev" }, - { "pattern": "*.xid.dev/console", "zone_name": "xid.dev" }, - { "pattern": "*.xid.dev/console/*", "zone_name": "xid.dev" }, - ], + "routes": [], "compatibility_date": "2026-07-27", "compatibility_flags": ["nodejs_compat"], "assets": { diff --git a/apps/server/worker/auth/password.ts b/apps/server/worker/auth/password.ts index 7f6e1679..8a214632 100644 --- a/apps/server/worker/auth/password.ts +++ b/apps/server/worker/auth/password.ts @@ -22,7 +22,7 @@ import { PASSWORD_RESET_TTL_MS } from '../lib/ttl' // --- 参数常量 --- const ARGON2_MEMORY_KB = 65536 // 64 MiB(password-auth rule) -const ARGON2_ITERATIONS = 3 +const ARGON2_ITERATIONS = 2 // Workers Free CPU 10ms 上限:改密需两次 Argon2(验旧+算新),3 迭代会超限 const ARGON2_HASH_LEN = 32 const ARGON2_PARALLELISM = 1 const MIN_LENGTH = 12 diff --git a/apps/server/worker/env.d.ts b/apps/server/worker/env.d.ts index bb5992f1..7836fc47 100644 --- a/apps/server/worker/env.d.ts +++ b/apps/server/worker/env.d.ts @@ -72,6 +72,8 @@ type WorkerBindings = { PEPPER: string // BOOTSTRAP_TOKEN:可选,seed/bootstrap 接口门控(配置则强制 X-Bootstrap-Token 匹配,防公网滥用) BOOTSTRAP_TOKEN?: string + // RESEND_API_KEY:可选,EMAIL_PROVIDER=resend 时经 Resend API 发事务邮件。 + RESEND_API_KEY?: string // Turnstile 必须成对配置:site key 是公开 runtime var,secret 是 Workers Secret。 // 两者都未配置时 dev/test 跳过;只配置任一项时认证配置与校验均 fail closed。 TURNSTILE_SITE_KEY?: string diff --git a/apps/server/worker/queues/email.ts b/apps/server/worker/queues/email.ts index c508a678..b495e366 100644 --- a/apps/server/worker/queues/email.ts +++ b/apps/server/worker/queues/email.ts @@ -62,6 +62,39 @@ export class CloudflareEmailProvider implements EmailProvider { } } +// Resend Provider:经 HTTPS API 发信,适用于无 Cloudflare Email Service 配额的自托管部署。 +// API Key 走 Workers Secret RESEND_API_KEY,不进代码与 D1。 +export class ResendEmailProvider implements EmailProvider { + readonly name = 'resend' + private readonly apiKey: string + + constructor(apiKey: string) { + this.apiKey = apiKey + } + + async send(input: EmailSendInput): Promise { + const resp = await fetch('https://api.resend.com/emails', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + from: `${input.from.name} <${input.from.email}>`, + to: [input.to], + subject: input.subject, + html: input.html, + text: input.text, + }), + signal: AbortSignal.timeout(15000), + }) + if (!resp.ok) { + const body = await resp.text().catch(() => '') + throw new Error(`resend_send_failed: ${resp.status} ${body.slice(0, 200)}`) + } + } +} + // 事务邮件模板(Mustache 子集)。按 type + locale 选模板。 // 真实部署模板由 R2 语言包提供(见 07 章 4);此处内置 en/zh-Hans 兜底,保证无外部依赖可渲染。 type EmailTemplate = { @@ -564,6 +597,11 @@ export function resolveEmailProvider(env: Env): EmailProvider { if (!isDevOrTestEnvironment(env)) throw new Error('email_provider_not_configured') return new TestEmailProvider(env) } + if (env.EMAIL_PROVIDER === 'resend') { + const apiKey = (env as unknown as { RESEND_API_KEY?: string }).RESEND_API_KEY + if (!apiKey) throw new Error('email_provider_not_configured') + return new ResendEmailProvider(apiKey) + } return new CloudflareEmailProvider(env.EMAIL) } diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 59849308..dd528006 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -1,17 +1,12 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "xid", - "account_id": "86e4d320a5d69fb54f9721fb219a4902", + "account_id": "f475b3dcf291c33ae82ab0b595f81958", "main": "worker/index.ts", "preview_urls": false, "keep_vars": true, "routes": [ - // Core 保留 apex Custom Domain、tenant fallback,以及 Cloudflare for SaaS custom hostname - // 所需的 zone-wide fallback。Site 与 Console 的更具体 routes 优先。 - // Site 与 Console 的具体 routes 由各自 wrangler.jsonc 唯一声明。 - { "pattern": "xid.dev", "custom_domain": true }, - { "pattern": "*.xid.dev/*", "zone_name": "xid.dev" }, - { "pattern": "*/*", "zone_name": "xid.dev" }, + { "pattern": "auth.otscup.com", "custom_domain": true }, ], "compatibility_date": "2025-04-08", "compatibility_flags": ["nodejs_compat"], @@ -45,14 +40,14 @@ { "binding": "DB", "database_name": "xid-db", - "database_id": "149e5997-e684-41d0-b483-afa6948479c3", + "database_id": "776b350f-6d9a-476b-b808-4ddbcc67752d", "migrations_dir": "../../packages/db/drizzle", }, ], "kv_namespaces": [ { "binding": "CACHE", - "id": "a827c644c20344aaa6c7760d76875122", + "id": "fa82fc300d44485c8536ee9b4cd8c8b3", }, ], "r2_buckets": [ @@ -62,12 +57,6 @@ }, ], "send_email": [{ "name": "EMAIL" }], - "analytics_engine_datasets": [ - { - "binding": "ANALYTICS", - "dataset": "xid_analytics", - }, - ], "queues": { "producers": [ { "binding": "EMAIL_QUEUE", "queue": "xid-email" }, @@ -274,9 +263,6 @@ "new_sqlite_classes": ["SessionHandoffDO"], }, ], - "triggers": { - "crons": ["0 * * * *", "0 2 * * *"], - }, "vars": { "ENVIRONMENT": "production", }, diff --git a/apps/site/wrangler.jsonc b/apps/site/wrangler.jsonc index 7aa7aecc..8301e8e3 100644 --- a/apps/site/wrangler.jsonc +++ b/apps/site/wrangler.jsonc @@ -1,102 +1,10 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "xid-site", - "account_id": "86e4d320a5d69fb54f9721fb219a4902", + "account_id": "f475b3dcf291c33ae82ab0b595f81958", "main": "worker/index.ts", "preview_urls": false, - "routes": [ - { "pattern": "xid.dev/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/index.md", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/index.mdx", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/docs", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/docs/index.md", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/docs/index.mdx", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/docs/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/status", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/status/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/status/index.md", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/status/index.mdx", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pricing", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pricing/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pricing/index.md", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pricing/index.mdx", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/getting-started", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/getting-started/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/hosted-auth", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/hosted-auth/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/organizations", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/organizations/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/oidc-oauth", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/oidc-oauth/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/enterprise-sso", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/enterprise-sso/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/social-login", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/social-login/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/management-api", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/management-api/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/webhooks", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/webhooks/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/branding", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/branding/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/scim", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/scim/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/scim/index.md", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/scim/index.mdx", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/saml", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/saml/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/sdks", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/sdks/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/self-hosting", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/self-hosting/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/zh-hans", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/zh-hans/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/zh-hans/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ja", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ja/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ja/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ko", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ko/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/ko/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/fr", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/fr/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/fr/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/de", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/de/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/de/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/es", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/es/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/es/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pt-br", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pt-br/", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pt-br/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/_astro/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/_nimbus/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/pagefind/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/og.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/og/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/robots.txt", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/sitemap-index.xml", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/sitemap-*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/sitemap.xml", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/llms.txt", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/llms-full.txt", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/en/llms.txt", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/en/llms-full.txt", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/favicon.ico", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/favicon-16.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/favicon-32.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/favicon-48.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/apple-touch-icon.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/icon-192.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/icon-512.png", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/site.webmanifest", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/brand/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/icons/*", "zone_name": "xid.dev" }, - { "pattern": "xid.dev/fonts/*", "zone_name": "xid.dev" }, - { "pattern": "www.xid.dev/*", "zone_name": "xid.dev" }, - { "pattern": "www.xid.dev/console", "zone_name": "xid.dev" }, - { "pattern": "www.xid.dev/console/*", "zone_name": "xid.dev" }, - ], + "routes": [], "compatibility_date": "2026-07-27", "assets": { "directory": "./dist", diff --git a/packages/types/src/env.ts b/packages/types/src/env.ts index 14fa3d64..0bce8ff1 100644 --- a/packages/types/src/env.ts +++ b/packages/types/src/env.ts @@ -17,7 +17,7 @@ export type EmailQueueMessage = { payload: Record } -export type EmailProviderName = 'cloudflare' | 'test' +export type EmailProviderName = 'cloudflare' | 'resend' | 'test' export type SmsProviderName = 'twilio' | 'vonage' | 'infobip' | 'messagebird' | 'test' export type WhatsappProviderName = 'twilio' | 'meta' | 'test' From a564f176a2f310e94736c2f53971da378a92eb9f Mon Sep 17 00:00:00 2001 From: otscup Date: Fri, 16 Oct 2026 19:11:02 +0800 Subject: [PATCH 2/2] feat(email): add Resend as transactional email provider