# Payment Kit — Cloudflare Workers 部署指南

> 本指南对应已经移除全部 mock 的版本（`8e0f092e` 之后）。所有身份验证、加密、签名、文件上传均走真实服务，不再支持本地 fake 凭据。

## 架构

```
Payment Kit Worker
  ├── D1 Database           # 业务数据（customers/invoices/subscriptions ...）
  ├── KV: DID_CONNECT_KV    # DID Connect session 临时存储
  ├── CF Queue              # 任务队列（fastq → CF Queue 适配）
  ├── Hyperdrive            # （可选）连接 Postgres，仅在启用时使用
  ├── Service Binding
  │    ├── AUTH_SERVICE → blocklet-service (BlockletServiceRPC)
  │    └── MEDIA_KIT    → media-kit
  └── Assets                # 前端 SPA（Vite 构建产物）
```

关键组件：

- **Hono** 替代 Express（路由 + 中间件适配层）
- **Sequelize-D1 shim** 替代 SQLite，表结构自动 sync
- **CF Queue** 替代 fastq，加 D1-native cron fallback
- **wrapAsyncListener** 追踪 `EventEmitter` async listener 的 Promise，防止 CF runtime 提前回收

---

## ⚠️ 必须先部署的前置服务

Payment Kit 自身**不做**任何身份验证、加密初始化、文件处理 —— 全部委托给依赖 Worker。**以下两个 Worker 必须先部署成功**，否则 Payment Kit 启动后 API 会 503 或无法加载 Stripe 密钥。

### 1. blocklet-service（即 DID Service）

Payment Kit 的身份来源，提供：

- `resolveIdentity(jwt, authHeader, appPid)` — 解析登录态，返回 `{ did, role, displayName, avatar }`
- `getAppEk(appPid)` — 提供 APP_EK（Stripe 密钥解密 + 其他敏感字段解密）
- `/__auth__/*` fetch fallback — DID Connect challenge / verify / token
- `component.call` 签名验证（通过 `X-Component-Sig` 回调 AUTH_SERVICE）

**要求**：

- 暴露 `BlockletServiceRPC` 作为 `entrypoint`
- 已注册 Payment Kit 的 `APP_PID`（拿到固定的 instance DID）
- EK 对应该 APP_PID 存在且未过期

**部署前置**：先完成 blocklet-service 的部署，拿到 service name（如 `blocklet-service`），并且确认 `resolveIdentity` / `getAppEk` RPC 方法可用。

### 2. media-kit

Payment Kit 的 upload/image CDN，用于：

- 商户 logo / 商品图片上传
- 发票附件、凭证下载
- 前端 `/api/uploads/*` 代理目标

**要求**：

- 暴露 HTTP endpoint（给 `MEDIA_KIT_URL` 用作 `__blocklet__.js` 的 `mediaKitUrl`）
- 同时导出 service binding `media-kit`（给 `MEDIA_KIT` binding 用，支持签名上传）

**部署前置**：先部署 media-kit 拿到 `workers.dev` URL 或自定义域名，填到 Payment Kit 的 `MEDIA_KIT_URL` 变量。

### 部署顺序

```
blocklet-service        →  已存在，在其他项目里先部署好
    │
    ├── 注册 Payment Kit APP_PID（得到 instance DID）
    ├── 生成/确认 APP_EK
    └── 确认 BlockletServiceRPC 方法 resolveIdentity / getAppEk 已实现
                     ↓
media-kit               →  部署并拿到 URL
                     ↓
Payment Kit Worker      →  本指南涵盖的部分
```

---

## 前置 Cloudflare 资源

在 Payment Kit 部署前创建以下资源，把生成的 ID 填到 `wrangler.jsonc`：

```bash
# 1. D1 数据库
wrangler d1 create payment-kit-prod
#   → 记下 database_id

# 2. KV Namespace（DID Connect session 存储）
wrangler kv namespace create DID_CONNECT_KV
#   → 记下 id

# 3. CF Queue + DLQ（任务队列）
wrangler queues create payment-kit-jobs
wrangler queues create payment-kit-jobs-dlq

# 4.（可选）Hyperdrive，仅当需要连接外部 Postgres 时
wrangler hyperdrive create payment-kit-hyperdrive --connection-string "postgres://..."
```

---

## 环境变量完整清单

### 两类变量

- **`vars`**：非敏感，写在 `wrangler.jsonc` 里，部署时一起提交。
- **`secrets`**：敏感，用 `wrangler secret put` 单独设置，不进源码。

### 完整清单

| 变量 | 类型 | 必需 | 说明 | 示例 |
|------|------|------|------|------|
| `APP_NAME` | var | ✅ | 应用显示名，出现在 header + `__blocklet__.js` | `"Payment Kit"` |
| `APP_PID` | var | ✅ | Instance DID，blocklet-service 注册时生成 | `"zNKuN3XwXN7xq2NsJQjjfwujyqCxx26DhwgV"` |
| `COMPONENT_DID` | var | ✅ | Payment Kit 组件 DID（固定值） | `"z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk"` |
| `MEDIA_KIT_URL` | var | ✅ | Media Kit Worker 公网 URL | `"https://media-kit.workers.dev"` |
| `APP_URL` | secret | ⚠️ 自定义域名时必需 | 应用对外 URL；不设则用 `workers.dev` 域名 | `"https://pay.example.com"` |
| `APP_SK` | secret | ✅ | 应用签名私钥（hex），DID Connect challenge 用 | `"0x..."` |
| `STRIPE_WEBHOOK_SECRET` | secret | ⚠️ 启用 Stripe 时必需 | Stripe Webhook 签名密钥 | `"whsec_..."` |

### 为什么这几个是必需的

- `APP_SK` — DID Connect challenge 的 ED25519 签名只能由 app 私钥完成；worker 拿不到 SK 就无法响应 `/api/did-connect/*` 登录流程。
- `APP_PID` — 所有 `AUTH_SERVICE.resolveIdentity(jwt, authHeader, APP_PID)` 调用都要传 instance DID；没有它 AUTH_SERVICE 无法定位该应用的 EK 和权限矩阵。
- `MEDIA_KIT_URL` — 前端 `__blocklet__.js` 初始化依赖这个 URL 渲染图片路径；缺失会导致 logo / 商品图 404。
- `STRIPE_WEBHOOK_SECRET` — CF 部署不再从 DB 读取（Blocklet Server 以前存 DB），**必须**用环境变量覆盖，否则 Stripe webhook 验签直接 400。

> **Stripe Secret Key 不需要单独设置**：存储在 `payment_methods.metadata` 里的加密 Stripe key 会在 Worker 首请求时通过 `AUTH_SERVICE.getAppEk(APP_PID)` 拿到 EK 后自动解密。

### 不需要设置的变量

以下历史变量不再需要，CF runtime 自动处理或从 AUTH_SERVICE 拉取：

- ~~`BLOCKLET_APP_EK`~~ — 运行时从 `AUTH_SERVICE.getAppEk(APP_PID)` 获取
- ~~`BLOCKLET_MODE`~~ — Worker 入口硬编码为 `production`
- ~~`BLOCKLET_APP_ID` / `BLOCKLET_APP_DID`~~ — 从 `APP_PID` 派生

---

## `wrangler.jsonc` 完整模板

```jsonc
{
  "name": "payment-kit",
  "main": "dist/worker.js",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "placement": { "mode": "smart" },

  "assets": {
    "directory": "public",
    "binding": "ASSETS",
    "html_handling": "auto-trailing-slash",
    "not_found_handling": "single-page-application"
  },

  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "payment-kit-prod",
      "database_id": "<your-d1-database-id>"
    }
  ],

  "kv_namespaces": [
    {
      "binding": "DID_CONNECT_KV",
      "id": "<your-kv-namespace-id>"
    }
  ],

  // 可选：启用 Hyperdrive（连接外部 Postgres）
  // "hyperdrive": [
  //   { "binding": "HYPERDRIVE", "id": "<your-hyperdrive-id>" }
  // ],

  "services": [
    { "binding": "MEDIA_KIT", "service": "media-kit" },
    {
      "binding": "AUTH_SERVICE",
      "service": "blocklet-service",
      "entrypoint": "BlockletServiceRPC"
    }
  ],

  "vars": {
    "APP_NAME": "Payment Kit",
    "APP_PID": "<your-instance-did>",
    "COMPONENT_DID": "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk",
    "MEDIA_KIT_URL": "https://<your-media-kit>.workers.dev"
  },

  "queues": {
    "producers": [
      { "binding": "JOB_QUEUE", "queue": "payment-kit-jobs" }
    ],
    "consumers": [
      {
        "queue": "payment-kit-jobs",
        "max_batch_size": 10,
        "max_batch_timeout": 5,
        "max_retries": 3,
        "dead_letter_queue": "payment-kit-jobs-dlq"
      }
    ]
  },

  "triggers": {
    "crons": ["* * * * *"]
  }
}
```

---

## 部署流程（按步骤）

### Phase 0：前置确认

```bash
# 确认 blocklet-service 已部署且暴露 BlockletServiceRPC
wrangler deployments list --name blocklet-service

# 确认 media-kit 已部署
wrangler deployments list --name media-kit

# 拿到 Payment Kit 的 APP_PID 和 APP_SK（从 blocklet-service admin 注册得到）
# APP_PID: instance DID
# APP_SK:  应用私钥 hex
```

### Phase 1：创建 CF 资源

```bash
cd blocklets/core/cloudflare
wrangler d1 create payment-kit-prod
wrangler kv namespace create DID_CONNECT_KV
wrangler queues create payment-kit-jobs
wrangler queues create payment-kit-jobs-dlq
```

把返回的 `database_id` / `kv namespace id` 填到 `wrangler.jsonc` 对应位置。

### Phase 2：填写 `wrangler.jsonc`

用 Phase 0 拿到的 `APP_PID` + Phase 1 创建的 D1/KV id + media-kit URL 填充上面的模板。

### Phase 3：设置 secrets

```bash
cd blocklets/core/cloudflare

# 必需
wrangler secret put APP_SK
#   粘贴应用私钥 hex

# 启用 Stripe 时必需（只有 webhook secret；Stripe API key 走 DB + EK 解密，不需要 env）
wrangler secret put STRIPE_WEBHOOK_SECRET
#   粘贴 whsec_...

# 自定义域名时必需
wrangler secret put APP_URL
#   粘贴 https://pay.example.com
```

### Phase 4：构建

```bash
cd blocklets/core

# 1. 前端 SPA
npx vite build --config cloudflare/vite.config.ts --outDir cloudflare/public

# 2. Worker 入口
cd cloudflare
node run-build.js
#   输出: dist/worker.js（~8.7MB）
```

### Phase 5：部署

```bash
cd blocklets/core/cloudflare
npx wrangler deploy --config wrangler.jsonc
```

### 一键脚本

```bash
cd blocklets/core && \
npx vite build --config cloudflare/vite.config.ts --outDir cloudflare/public && \
cd cloudflare && \
node run-build.js && \
npx wrangler deploy --config wrangler.jsonc
```

### Phase 6：首次启动验证

部署后访问 `https://<your-worker>.workers.dev/__blocklet__.js`，应当返回：

```js
window.blocklet = { appName: 'Payment Kit', appUrl: '...', appPid: '...', ... }
```

访问 `https://<your-worker>.workers.dev/api/health`（若存在）或任意 public API，确认 200。

**首次请求会触发**：

1. `ensureModelsInit()` — Sequelize-D1 sync，在 D1 里自动建表
2. `initFromAuthService(env)` — 从 `AUTH_SERVICE.getAppEk(APP_PID)` 拉 EK 并初始化 crypto chain（`PBKDF2 → AES password`）
3. DID Connect 路由挂载 — `APP_SK` + `DID_CONNECT_KV` 都就绪才会挂载

查看首请求日志：

```bash
wrangler tail --format pretty
```

应当看到：

```
[Security] Initialized with EK from AUTH_SERVICE
```

如果看到：

```
[Security] No APP_EK found for instance <APP_PID>
```

→ blocklet-service 里该 APP_PID 没有 EK，回到 Phase 0 重新注册。

如果看到：

```
[Security] initFromAuthService failed: ...
```

→ `AUTH_SERVICE` binding 或 `getAppEk` RPC 方法有问题，检查 `wrangler.jsonc` 的 `entrypoint` 是否为 `BlockletServiceRPC`。

---

## Stripe 集成

### Webhook endpoint 切换

从 Blocklet Server 迁移到 CF Worker 时，**必须**在 Stripe Dashboard 做以下改动：

1. 创建新的 webhook endpoint，URL 改成 `https://<your-worker>.workers.dev/api/stripe/webhook`
2. 复制新 endpoint 的 signing secret（`whsec_...`）
3. 用 `wrangler secret put STRIPE_WEBHOOK_SECRET` 设置到 Worker
4. 确认 webhook events 勾选：
   - `payment_intent.succeeded`
   - `payment_intent.payment_failed`
   - `invoice.payment_succeeded`
   - `invoice.payment_failed`
   - `customer.subscription.*`
5. **不要**复用旧的 webhook secret —— Stripe 每个 endpoint 有独立 signing key

### Stripe key 解密

Payment Kit 源代码从 DB 读取加密的 Stripe key，再用 APP_EK 派生的 password 解密。CF 下这条链路保留：

```
首请求 → initFromAuthService(env)
       → AUTH_SERVICE.getAppEk(APP_PID)
       → PBKDF2(appEk, APP_PID, 256, 32, sha512)
       → 保存到 _password

Stripe 调用 → PaymentMethod.getStripeClient()
           → decrypt(encryptedKey, _password)
           → new Stripe(plaintextKey)
```

只要 `APP_PID` 复用、`AUTH_SERVICE.getAppEk` 返回的 EK 和原 Blocklet Server 一致，这条链路自动走通，不需要额外设置环境变量。

---

## D1 数据库初始化

Payment Kit 使用 Sequelize 的 `sync()` 自动建表，首次部署后访问任意 API 即触发。

```bash
# 查看已建的表
wrangler d1 execute payment-kit-prod --remote \
  --command "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"

# 查看特定表结构
wrangler d1 execute payment-kit-prod --remote \
  --command "PRAGMA table_info(customers);"

# 检查某个关键迁移（示例：locks 表）
wrangler d1 execute payment-kit-prod --remote \
  --command "SELECT COUNT(*) FROM locks;"
```

如果需要从 Blocklet Server 迁移现有数据，参考 `MIGRATION-RUNBOOK.md` 的 Phase 3-4。

---

## Mount Point 模式

当 Payment Kit 被其他 Worker（如 AIGNE Hub）通过 Service Binding 以 mount point 挂载（如 `/payment/*`）时，Payment Kit 自动：

1. 从 `X-Mount-Prefix` header 读取挂载前缀
2. `__blocklet__.js` 返回正确的 `prefix` 和 `groupPrefix`
3. 前端 `window.blocklet.prefix` 由 gateway 重写

集成方式详见 `test-aigne-hub/README.md`。

---

## 认证流程

```
浏览器 → Payment Kit Worker
  ├── Hono caller middleware
  │    ├── 从 Cookie 取 login_token JWT
  │    ├── AUTH_SERVICE.resolveIdentity(jwt, authHeader, APP_PID)
  │    └── 缓存 { did, role, displayName, avatar } 到 Hono context
  ├── Express adapter
  │    ├── 注入 req.user + x-user-* headers
  │    └── security.ts 按 role 检查权限
  └── 路由处理
```

- `component.call` 路径（`x-component-sig`）：CF 下委托给 AUTH_SERVICE，不做本地 ED25519 验签
- Service Binding 调用：通过 Bearer access-key 或 Cookie JWT 认证，走同一条 AUTH_SERVICE 验证链路

---

## 常见问题

### Worker 启动返回 503 "AUTH_SERVICE not configured"

- `wrangler.jsonc` 里 `services[].binding` 必须是 `AUTH_SERVICE`（全大写、准确拼写）
- 目标 service name 必须与 blocklet-service 的 Worker name 一致
- `entrypoint` 必须是 `BlockletServiceRPC`

### 首请求日志打 "No APP_EK found for instance"

- Blocklet Server admin 侧需要确认该 APP_PID 已经注册并生成了 EK
- 多次重启或重新注册过应用时，旧的 APP_PID 可能失效 → 拿新的 APP_PID 更新 `wrangler.jsonc`

### Stripe webhook 永远 400 "signature verification failed"

- `STRIPE_WEBHOOK_SECRET` 一定是 CF Worker endpoint 对应的那个 `whsec_...`，不是 Blocklet Server 旧的
- 如果 Stripe Dashboard 里还指向旧 URL，新 endpoint 永远收不到事件

### 前端图片/logo 404

- 检查 `MEDIA_KIT_URL` 是否指向可访问的 Media Kit Worker
- 确认 media-kit Worker 已经部署并且 public endpoint 可用

### DID Connect 登录卡在 challenge 阶段

- `APP_SK` 必须是 hex 字符串，对应的 public key 必须在 blocklet-service 里注册为该 APP_PID 的 key
- `DID_CONNECT_KV` 必须已创建并 binding 正确

---

## 目录结构

```
blocklets/core/cloudflare/
  ├── worker.ts              # Worker 入口（Hono 路由 + Express 适配）
  ├── run-build.js           # esbuild 构建脚本
  ├── build.ts               # 构建配置 + shim 别名映射
  ├── vite.config.ts         # 前端 Vite 构建配置
  ├── did-connect-auth.ts    # DID Connect 登录路由（挂载需要 APP_SK + DID_CONNECT_KV）
  ├── wrangler.jsonc         # 生产配置模板
  ├── wrangler.migration-test.json # 迁移测试环境配置
  ├── index.html             # 前端开发入口
  ├── public/                # 前端构建产物（Vite 输出）
  ├── dist/                  # Worker 构建产物（esbuild 输出）
  ├── shims/                 # Node.js → CF Workers 适配层
  │   ├── blocklet-sdk/
  │   │   ├── security.ts    #   EK 初始化 + encrypt/decrypt（走 AUTH_SERVICE.getAppEk）
  │   │   ├── verify-sign.ts #   组件签名验证（委托 AUTH_SERVICE）
  │   │   └── ...
  │   ├── sequelize-d1/      #   Sequelize → D1 适配
  │   ├── queue.ts           #   fastq → CF Queue 适配
  │   ├── lock.ts            #   分布式锁（D1 实现）
  │   ├── cron.ts            #   定时任务
  │   └── ...
  └── test-aigne-hub/        # Gateway 集成参考实现
      ├── worker.ts
      ├── wrangler.json
      └── README.md          # 集成指南
```

---

## 相关文档

- `MIGRATION-RUNBOOK.md` — 从 Blocklet Server 迁移到 CF Workers 的完整流程（数据迁移、切流、回滚）
- `MIGRATION-CHALLENGES.md` — 迁移过程遇到的典型问题和解决方案
- `test-aigne-hub/README.md` — Service Binding mount point 集成参考
