mass_mail_engine/docs/OPENAPI.md
warrenchen 60a24ee7c0 feat: Enhance Send Engine API with JWT and OIDC support, update OpenAPI documentation. Complete send flow with mock.
- Updated authentication to support OAuth2 Client Credentials and JWT with OIDC/JWKS verification.
- Added necessary claims for JWT, including `aud` and fallback for JWT metadata.
- Improved error response format and added additional claims for webhook events.
- Introduced new request body structure for creating send jobs, supporting both snake_case and camelCase.
- Implemented DevMockSenderWorker for simulating email sending during development.
- Integrated Amazon SES for email delivery, with bulk sending capabilities.
- Updated OpenAPI documentation to reflect changes in request and response schemas.
- Enhanced error handling and logging for better traceability.
2026-02-25 14:33:34 +09:00

366 lines
9.8 KiB
Markdown
Raw 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.

# OpenAPI Notes
本文件描述 Send Engine 對外 API、Webhook 驗證與與 Member Center 的介接規則。
目標是讓 Member Center 與租戶網站可以清楚交換資料與責任邊界。
## Auth 與驗證
### 1. 租戶網站 → Send Engine API
使用 OAuth2 Client Credentialstoken 由 Member Center 簽發Send Engine 以 OIDC/JWKS 驗簽驗證)。
若未明確設定 JWT metadata/authority會回退使用 `MemberCenter__BaseUrl + /.well-known/openid-configuration`
必要 claims
- `tenant_id`
- `scope`(至少 `newsletter:send.write`
- 必須包含 `aud`(需符合 `Jwt__Audience`
規則:
- `tenant_id` 只能取自 token不接受 body 覆寫
- `list_id` 必須屬於該 tenant
### 2. Member Center → Send Engine Webhook
使用簽名 WebhookHMAC或 OAuth2 Client Credentials建議簽名
Header 建議:
- `X-Signature`: `hex(hmac_sha256(secret, body))`
- `X-Timestamp`: Unix epoch seconds
- `X-Nonce`: UUID
- `X-Client-Id`: Auth client UUID對應 `auth_clients.id`
驗證規則:
- timestamp 在允許時間窗內(例如 ±5 分鐘)
- nonce 不可重複(重放防護)
- signature 必須匹配
- client 必須存在且為 active
- `auth_clients.tenant_id` 必須與 payload `tenant_id` 一致
- 不使用任何 `X-Client-Id` fallback缺少 tenant 對應 client 時應由來源端跳過發送
- 預設拒絕 `auth_clients.tenant_id = NULL` 的通用 client可透過 `Webhook__AllowNullTenantClient=true` 明確開啟)
### 3. Send Engine → Member Center 回寫
使用 OAuth2 Client CredentialsSend Engine 作為 client
scope 最小化:
- `newsletter:events.write`(停用回寫)
- `newsletter:list.read`(若未來仍需查詢)
實作約定:
- 優先走 token endpointclient credentials
- `ApiToken` 僅作暫時 fallback建議逐步移除
## 通用欄位
### Timestamp
- 欄位:`occurred_at`
- 格式RFC3339例如 `2026-02-10T09:30:00Z`
### ID
- `tenant_id``list_id``subscriber_id``event_id` 皆為 UUID
## 通用錯誤格式
```json
{
"error": "string_code"
}
```
補充:
- 部分錯誤會附帶 `reason``message`(例如 webhook 驗證失敗)
- `message``request_id` 目前非固定欄位
## WebhookMember Center → Send Engine
### A. 訂閱事件(增量)
用途:同步新增/取消/偏好變更。
Endpoint
- `POST /webhooks/subscriptions`
Scope
- `newsletter:events.write`
事件型別:
- `subscription.activated`
- `subscription.unsubscribed`
- `preferences.updated`
Request Body示意
```json
{
"event_id": "uuid",
"event_type": "subscription.activated",
"tenant_id": "uuid",
"list_id": "uuid",
"subscriber": {
"id": "uuid",
"email": "user@example.com",
"status": "active",
"preferences": { "topic": "news" }
},
"occurred_at": "2026-02-10T09:30:00Z"
}
```
Response
- `200 OK`accepted
- `401/403`:驗證失敗
- `409`event_id 重複或 nonce 重放
- `422`:格式錯誤
### B. 全量名單同步
用途:由 Member Center 主動推送全量或分批名單,避免 Send Engine 拉取名單。
Endpoint
- `POST /webhooks/lists/full-sync`
Scope
- `newsletter:events.write`
Request Body分批示意
```json
{
"sync_id": "uuid",
"batch_no": 1,
"batch_total": 10,
"tenant_id": "uuid",
"list_id": "uuid",
"subscribers": [
{ "id": "uuid", "email": "a@example.com", "status": "active" },
{ "id": "uuid", "email": "b@example.com", "status": "unsubscribed" }
],
"occurred_at": "2026-02-10T09:30:00Z"
}
```
Response
- `200 OK`accepted
- `401/403`:驗證失敗
- `409`sync_id + batch_no 重複
- `422`:格式錯誤
## API租戶網站 → Send Engine
### C. 建立 Send Job
用途:租戶網站送入內容,建立 Campaign/Send Job 並排程。
Endpoint
- `POST /api/send-jobs`
Scope
- `newsletter:send.write`
Request Body
```json
{
"tenant_id": "uuid",
"list_id": "uuid",
"name": "Weekly Update",
"subject": "Weekly Update",
"body_html": "<p>Hello</p>",
"body_text": "Hello",
"template": null,
"scheduled_at": "2026-02-11T02:00:00Z",
"window_start": "2026-02-11T02:00:00Z",
"window_end": "2026-02-11T05:00:00Z",
"tracking": { "open": true, "click": false }
}
```
欄位規則:
- `subject` 必填,最小長度 1
- `body_html` / `body_text` / `template` 至少擇一
- `window_start` 必須小於 `window_end`(若有提供)
- `tenant_id` 必須已存在(不存在回 `422 tenant_not_found`
- `list_id` 若不存在,會在該 tenant 下自動建立 placeholder list 後建立 send job
- `template` 可攜帶替換參數(例如:`{"unsubscribe_url":"https://member.example/unsub?email={{email}}","ses_template_name":"newsletter_default"}`
替換合約Mock/SES 一致):
- `{{email}}`
- `{{unsubscribe_url}}`
- `{{unsubscribe_token}}`
- `{{tenant_id}}`
- `{{list_id}}`
- `{{campaign_id}}`
- `{{send_job_id}}`
Response
```json
{
"send_job_id": "uuid",
"sendJobId": "uuid",
"status": "pending"
}
```
說明:回應同時提供 `snake_case``camelCase`(向後相容不同語言客戶端)。
### C-1. Tenant Site Integration已實作
用途:內容網站以 Member Center 發出的 JWT 呼叫 Send Engine 建立發送工作。
步驟:
1. 取得 access tokenscope 至少 `newsletter:send.write`
2. 呼叫 `POST /api/send-jobs`
3. 查詢 `GET /api/send-jobs/{id}`(需 `newsletter:send.read`
token 需包含:
- `tenant_id`UUID
- `scope`(空白分隔字串)
範例(建立 send job
```bash
curl -X POST "http://localhost:6060/api/send-jobs" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "c9034414-43d6-404e-8d41-e80922420bf1",
"list_id": "22222222-2222-2222-2222-222222222222",
"name": "Weekly Update",
"subject": "Hi {{email}}",
"body_html": "<p>Hello {{email}}</p><p><a href=\"{{unsubscribe_url}}\">unsubscribe</a></p>",
"body_text": "Hello {{email}} | unsubscribe: {{unsubscribe_url}}",
"template": {
"unsubscribe_url": "https://member.example/unsubscribe?email={{email}}",
"ses_template_name": "newsletter_default"
}
}'
```
回應:
```json
{
"send_job_id": "uuid",
"sendJobId": "uuid",
"status": "pending"
}
```
說明:
- `ESP__Provider=mock`(非 ses會由 Dev Sender 產生 `send.preview` 事件供你檢查替換結果
- `ESP__Provider=mock` 時,也會把每位收件人的模擬發送內容輸出到 console log`MOCK send preview`
- `ESP__Provider=ses` 時,背景 sender 會用 SES `SendBulkEmail`(每批最多 50
- 若已設定 Member Center one-click token endpoint發送前會批次呼叫 `POST /newsletter/one-click-unsubscribe-tokens`
-`status=issued` 的收件者會被送出,並把 `unsubscribe_token` 注入替換內容
### D. 查詢 Send Job
Endpoint
- `GET /api/send-jobs/{id}`
Scope
- `newsletter:send.read`
Response
```json
{
"id": "uuid",
"tenant_id": "uuid",
"tenantId": "uuid",
"list_id": "uuid",
"listId": "uuid",
"campaign_id": "uuid",
"campaignId": "uuid",
"status": "running",
"scheduled_at": "2026-02-11T02:00:00Z",
"scheduledAt": "2026-02-11T02:00:00Z",
"window_start": "2026-02-11T02:00:00Z",
"windowStart": "2026-02-11T02:00:00Z",
"window_end": "2026-02-11T05:00:00Z",
"windowEnd": "2026-02-11T05:00:00Z"
}
```
### E. 取消 Send Job
Endpoint
- `POST /api/send-jobs/{id}/cancel`
Scope
- `newsletter:send.write`
Response
```json
{
"id": "uuid",
"status": "cancelled"
}
```
## WebhookSES → Send Engine
### F. SES 事件回報
用途:接收 bounce/hard_bounced/soft_bounced/complaint/suppression/delivery/open/click 等事件。
推薦架構(正式):
- `SES Configuration Set -> SNS -> SQS -> ECS Worker`
- 由 Worker 消費事件,不要求對外公開 webhook
相容模式(可選):
- `POST /webhooks/ses`
驗證:
- 目前實作:`Ses__SkipSignatureValidation=false` 時僅要求 `X-Amz-Sns-Signature` header 存在
- 正式建議:補上 SES/SNS 憑證鏈與簽章內容驗證
Request Body示意
```json
{
"event_type": "bounce",
"message_id": "ses-id",
"tenant_id": "uuid",
"email": "user@example.com",
"bounce_type": "hard",
"occurred_at": "2026-02-10T09:45:00Z"
}
```
Response
- `200 OK`
事件對應規則(固定):
- `hard_bounced`:立即設為黑名單(`suppressed`
- `soft_bounced`:累計達門檻後設為黑名單(`suppressed`
- `complaint`:取消訂閱並回寫 Member Center
- `suppression`:設為黑名單(`suppressed`
回寫 Member Center 條件:
- `hard_bounced`:設黑名單後回寫
- `soft_bounced`:達門檻設黑名單後回寫
- `complaint`:立即回寫
- `suppression`:設黑名單後回寫
回寫原因碼(固定):
- `hard_bounce`
- `soft_bounce_threshold`
- `complaint`
- `suppression`
## 外部 APISend Engine → Member Center
以下為 Member Center 端提供的 API非 Send Engine 的 OpenAPI 規格範圍。
### G. 停用訂閱回寫
用途:因 hard bounce / complaint 停用訂閱,並在 Member Center 註記來源。
EndpointMember Center 提供):
- `POST /subscriptions/disable`
Scope
- `newsletter:events.write`
Request Body示意
```json
{
"tenant_id": "uuid",
"subscriber_id": "uuid",
"list_id": "uuid",
"reason": "hard_bounce",
"disabled_by": "send_engine",
"occurred_at": "2026-02-10T09:45:00Z"
}
```
## 狀態碼與錯誤
通用錯誤:
- `401/403`Auth 或 scope 不符
- `409`重放或事件重複nonce / event_id
- `422`:資料格式錯誤
- `500`:伺服器內部錯誤
## Retry 策略(整合規格)
- Throttle指數退避重試
- Temporary network error重試
- Hard failure不重試
- Retry 上限可設定(例如 5 次)
## 相關環境參數
- `Bounce__SoftBounceThreshold`soft bounce 轉黑名單門檻(預設 `5`