diff --git a/docker-compose.yml b/docker-compose.yml index 3e6396b..26499e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,30 +72,28 @@ services: - MINIO_REGION=${MINIO_REGION:-us-east-1} - MINIO_LIFECYCLE_DAYS=${MINIO_LIFECYCLE_DAYS:-7} - # === OAuth / Member Center(必填,缺漏 fail-fast)=== - - MEMBER_CENTER_ISSUER=${MEMBER_CENTER_ISSUER} - - MEMBER_CENTER_JWKS_URL=${MEMBER_CENTER_JWKS_URL} + # === Phase 0.8b A2:visionA → converter API key 認證(1:1 internal trust)=== + # 不能給 default — 必須使用者明示設置;未設時 apiKeyMiddleware 對外 API 一律回 503。 + # 兩端(converter / visionA)值必須完全一致;rotate 時雙端同時更新。 + - CONVERTER_API_KEY=${CONVERTER_API_KEY} + + # === Phase 0.8b A7:trust proxy(audit log forensic source_ip)=== + # stage Nginx 1 hop → 1;prod cloud LB + Nginx → 2;明確 CIDR 也支援 + - TRUST_PROXY=${TRUST_PROXY:-loopback} + + # === Member Center Token Endpoint(converter → FAA OAuth client 用,保留)=== + # visionA → converter 已改 API key 不再走 OAuth/JWKS;但 converter promote + # 階段仍以 client_credentials grant 跟 Member Center 取 token、再 PUT 到 FAA。 - MEMBER_CENTER_TOKEN_URL=${MEMBER_CENTER_TOKEN_URL} - # === Converter 身份(必填)=== - - KNERON_CONVERTER_AUDIENCE=${KNERON_CONVERTER_AUDIENCE} + # === Converter OAuth Client 身份(converter → FAA 用,必填)=== - KNERON_CONVERTER_CLIENT_ID=${KNERON_CONVERTER_CLIENT_ID} - KNERON_CONVERTER_CLIENT_SECRET=${KNERON_CONVERTER_CLIENT_SECRET} - - CONVERTER_TENANT_ID=${CONVERTER_TENANT_ID:-} # === File Access Agent(必填)=== - FILE_ACCESS_AGENT_BASE_URL=${FILE_ACCESS_AGENT_BASE_URL} - FILE_ACCESS_AGENT_AUDIENCE=${FILE_ACCESS_AGENT_AUDIENCE} - # === Scope(可選,預設 TDD §8)=== - - CONVERTER_SCOPE_WRITE=${CONVERTER_SCOPE_WRITE:-converter:job.write} - - CONVERTER_SCOPE_READ=${CONVERTER_SCOPE_READ:-converter:job.read} - - # === JWKS / JWT cache 行為(可選)=== - - JWKS_CACHE_MAX_AGE_MS=${JWKS_CACHE_MAX_AGE_MS:-600000} - - JWKS_COOLDOWN_MS=${JWKS_COOLDOWN_MS:-30000} - - JWT_CLOCK_TOLERANCE_SEC=${JWT_CLOCK_TOLERANCE_SEC:-60} - # === OAuth Client cache(可選)=== - OAUTH_TOKEN_REFRESH_SKEW_MS=${OAUTH_TOKEN_REFRESH_SKEW_MS:-60000} - OAUTH_TOKEN_TIMEOUT_MS=${OAUTH_TOKEN_TIMEOUT_MS:-10000} @@ -111,6 +109,22 @@ services: # === Upload concurrency(T10 修 D5)=== - MAX_CONCURRENT_UPLOADS=${MAX_CONCURRENT_UPLOADS:-5} - UPLOAD_RETRY_AFTER_SECONDS=${UPLOAD_RETRY_AFTER_SECONDS:-30} + + # === Phase 0.8b Phase B:/result rate limit(per token_fingerprint)=== + # 命名以 §16.2.1 canonical names 為準(_PER_MIN + _WINDOW_MS 配對、不用 _MAX) + - RESULT_RATE_LIMIT_BURST_PER_10S=${RESULT_RATE_LIMIT_BURST_PER_10S:-5} + - RESULT_RATE_LIMIT_BURST_WINDOW_MS=${RESULT_RATE_LIMIT_BURST_WINDOW_MS:-10000} + - RESULT_RATE_LIMIT_SUSTAINED_PER_MIN=${RESULT_RATE_LIMIT_SUSTAINED_PER_MIN:-20} + - RESULT_RATE_LIMIT_SUSTAINED_WINDOW_MS=${RESULT_RATE_LIMIT_SUSTAINED_WINDOW_MS:-60000} + + # === Phase 0.8b Phase B:/result bandwidth quota(保護 mass download)=== + # 1 GiB = 1073741824 / 6 GiB = 6442450944 + - RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES=${RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES:-1073741824} + - RESULT_BANDWIDTH_QUOTA_PER_DAY_BYTES=${RESULT_BANDWIDTH_QUOTA_PER_DAY_BYTES:-6442450944} + + # === Phase 0.8b Phase B:/result concurrent stream cap + stream timeout === + - MAX_CONCURRENT_RESULT_STREAMS=${MAX_CONCURRENT_RESULT_STREAMS:-10} + - RESULT_STREAM_TIMEOUT_MS=${RESULT_STREAM_TIMEOUT_MS:-300000} restart: unless-stopped # ---------- Workers (stub mode) ---------- diff --git a/docs/autoflow/04-architecture/api/api-result.md b/docs/autoflow/04-architecture/api/api-result.md index e77d76e..6f3edaf 100644 --- a/docs/autoflow/04-architecture/api/api-result.md +++ b/docs/autoflow/04-architecture/api/api-result.md @@ -1511,3 +1511,102 @@ function createResultStreamConcurrencyLimiter({ maxConcurrent, retryAfterSeconds | R10 | OpenAPI spec 更新含 429 / 503 等新 status code | 看 `docs/openapi.yaml` | | R11 | 6 個新 integration test(IT-2 到 IT-7)全寫 + 4xx 系列原 test 保留 | grep test 檔 | | R12 | 6 個新 env 在 README / deploy doc 有文件化 | 看 README + `.env.example` | + +--- + +## 16. Env Naming Reference Table(**Architect 2026-05-18 拍板、避免三方命名漂移**) + +### 16.1 為什麼這節存在 + +Phase 0.8b Phase B deploy 時 DevOps 發現 Orchestrator prompt、source code、`env.example` §17 三方 env 命名不一致(例:Orchestrator 寫 `RESULT_RATE_LIMIT_SUSTAINED_MAX`、source code 讀 `RESULT_RATE_LIMIT_SUSTAINED_PER_MIN` + 額外 `_WINDOW_MS`)。此節提供**權威 canonical 命名清單**,後續任何 deployment 設定、docker-compose env 注入、`.env*` 模板生成、Orchestrator 派任務、文件互引,全部以本節為準。 + +**拍板原則**:以 source code 實際讀的命名為準(改 code 風險 > 改 deployment)。本表已對照 `apps/task-scheduler/src/routes/v1/index.js` L116-169、`apps/task-scheduler/src/routes/v1/result.js` L60、`apps/task-scheduler/src/config.js` L112-296、`apps/task-scheduler/src/services/jobService.js` L69、`apps/task-scheduler/src/storage/{local,minio}.js`、`apps/task-scheduler/src/auth/apiKeyMiddleware.js` 與 `apps/task-scheduler/env.example` 三方確認。 + +### 16.2 Canonical 命名清單(全部 task-scheduler 讀的 env) + +#### 16.2.1 `/result` 端點專屬(Phase 0.8b Phase B 新增、本檔 spec 範圍) + +| Canonical env name | Required? | Default | Purpose | Source code reads | +|--------------------|-----------|---------|---------|-------------------| +| `RESULT_STREAM_TIMEOUT_MS` | optional | `300000`(5 min)| AC-7 stream response timeout(§14.3 / §15.2)| `src/routes/v1/result.js` L60(`getStreamTimeoutMs` lazy 讀)| +| `MAX_CONCURRENT_RESULT_STREAMS` | optional | `10` | AC-4 concurrent stream cap(§14.1 / §15.3);per-instance counter;超限 503 + `Retry-After: 30` | `src/routes/v1/index.js` L128(`parseEnvInt` 解析後傳給 `createResultStreamConcurrencyLimiter`)| +| `RESULT_RATE_LIMIT_BURST_PER_10S` | optional | `5` | AC-2 burst rate limit `max`(§9.2 / §14.1);per `token_fingerprint` | `src/routes/v1/index.js` L160 | +| `RESULT_RATE_LIMIT_BURST_WINDOW_MS` | optional | `10000`(10 s)| AC-2 burst rate limit `windowMs`;與 `_PER_10S` 成對 | `src/routes/v1/index.js` L159 | +| `RESULT_RATE_LIMIT_SUSTAINED_PER_MIN` | optional | `20` | AC-2 sustained rate limit `max`(§9.2 / §14.1);per `token_fingerprint` | `src/routes/v1/index.js` L169 | +| `RESULT_RATE_LIMIT_SUSTAINED_WINDOW_MS` | optional | `60000`(1 min)| AC-2 sustained rate limit `windowMs`;與 `_PER_MIN` 成對 | `src/routes/v1/index.js` L167 | +| `RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES` | optional | `1073741824`(1 GB)| AC-3 hourly bandwidth quota(§9.4 / §14.1);per `token_fingerprint` | `src/routes/v1/index.js` L116 | +| `RESULT_BANDWIDTH_QUOTA_PER_DAY_BYTES` | optional | `6442450944`(6 GB)| AC-3 daily bandwidth quota;per `token_fingerprint` | `src/routes/v1/index.js` L119 | + +**共 8 個 `RESULT_*` / `MAX_CONCURRENT_RESULT_STREAMS`。所有 8 個皆 optional(無設值會走 source code 內 fallback default)。** + +#### 16.2.2 其他 task-scheduler 讀的 env(非 `/result` 專屬、列出避免後續混淆) + +| Canonical env name | Required? | Default | Purpose | Source code reads | +|--------------------|-----------|---------|---------|-------------------| +| `CONVERTER_API_KEY` | optional(warn-only)| `''`(空字串)| visionA → converter 對外 API 認證 pre-shared key;未設時 `apiKeyMiddleware` 一律回 503 `service_unavailable` | `src/config.js` L167 + `src/auth/apiKeyMiddleware.js` L234 | +| `TRUST_PROXY` | optional | `'loopback'` | Express `app.set('trust proxy', ...)`;影響 `req.ip` 與 audit log `source_ip`(接受值:boolean / integer / 字串 keyword / CIDR)| `src/config.js` L145 + `src/app.js` | +| `PORT` | optional | `4000` | HTTP listen port | server entry(未經 config.js)| +| `NODE_ENV` | optional | `'development'` | 影響 FAA URL 強制 HTTPS 等行為 | `src/config.js` L212 | +| `LOG_LEVEL` | optional | `'info'` | log 等級 | server entry(未經 config.js)| +| `REDIS_URL` | optional | `'redis://localhost:6379'` | Redis 連線字串 | `src/redis.js` L24 | +| `FRONTEND_URL` | optional | `'http://localhost:3000'` | CORS origin | `src/app.js` L59 | +| `JOB_DATA_DIR` | optional | `'/data/jobs'` | local storage 與 worker 共用 volume 路徑 | `src/services/jobService.js` L69、`src/storage/local.js` L23 | +| `STORAGE_BACKEND` | optional | `'local'` | `'local'` / `'minio'` | `src/app.js` L170、`src/storage/minio.js` L39、`src/routes/v1/jobs.js` L945 | +| `MINIO_ENDPOINT_URL` | conditional(`STORAGE_BACKEND=minio` 必填)| `'http://192.168.0.130:9000'` | MinIO endpoint | `src/storage/minio.js` L40 | +| `MINIO_BUCKET` | conditional | `'convertet-working-space'` | MinIO bucket name | `src/storage/minio.js` L41 | +| `MINIO_ACCESS_KEY` | conditional | `'convuser'` | MinIO access key | `src/storage/minio.js` L42 | +| `MINIO_SECRET_KEY` | conditional | `''`(空字串)| MinIO secret key | `src/storage/minio.js` L43 | +| `MINIO_REGION` | conditional | `'us-east-1'` | MinIO region | `src/storage/minio.js` L44 | +| `MINIO_LIFECYCLE_DAYS` | optional | (TBD 由 bucket lifecycle policy 設)| bucket lifecycle 天數;orphan 清除 | env.example §6(非 task-scheduler 讀、由 init script 用)| +| `MEMBER_CENTER_TOKEN_URL` | **required** | — | converter → FAA OAuth client token endpoint(缺漏 → fail-fast)| `src/config.js` L112 | +| `KNERON_CONVERTER_CLIENT_ID` | **required** | — | converter OAuth client_id(缺漏 → fail-fast)| `src/config.js` L116 | +| `KNERON_CONVERTER_CLIENT_SECRET` | **required** | — | converter OAuth client_secret(缺漏 → fail-fast)| `src/config.js` L117 | +| `FILE_ACCESS_AGENT_BASE_URL` | **required** | — | FAA base URL;prod 強制 https | `src/config.js` L197 | +| `FILE_ACCESS_AGENT_AUDIENCE` | **required** | — | FAA OAuth audience | `src/config.js` L198 | +| `PROMOTE_TIMEOUT_MS` | optional | `300000`(300 s)| FAA PUT timeout | `src/config.js` L220 + `src/app.js` L125 | +| `OAUTH_TOKEN_REFRESH_SKEW_MS` | optional | `60000`(60 s)| OAuth token 距 expiresAt 還剩多少 ms 主動 refresh | `src/config.js` L225 | +| `OAUTH_TOKEN_TIMEOUT_MS` | optional | `10000`(10 s)| OAuth token endpoint timeout | `src/config.js` L228 | +| `MULTIPART_MODEL_MAX_BYTES` | optional | `524288000`(500 MB)| multer model file size 上限 | `src/config.js` L243 | +| `MULTIPART_REF_IMAGE_MAX_BYTES` | optional | `10485760`(10 MB)| 單張 ref_image 上限 | `src/config.js` L252 | +| `MULTIPART_REF_IMAGES_MAX_COUNT` | optional | `100` | ref_images 張數上限 | `src/config.js` L261 | +| `MAX_CONCURRENT_UPLOADS` | optional | `5` | 同時間最多進行幾個 upload;超限 503 + `Retry-After` | `src/config.js` L285 | +| `UPLOAD_RETRY_AFTER_SECONDS` | optional | `30` | upload 超限的 `Retry-After` 秒數 | `src/config.js` L291 | +| `API_V1_RATE_LIMIT_WINDOW_MS` | optional | `300000`(5 min)| per-clientId rate limit window;當前 clientId 寫死 `'visionA-service'` | env.example §15(middleware 預設可被 deps 覆寫)| +| `API_V1_RATE_LIMIT_MAX` | optional | `300` | per-clientId rate limit max | env.example §15 | + +### 16.3 命名規則約定(後續新增 env 必遵) + +- **scope prefix**:與 `/result` 端點相關用 `RESULT_*`、與 upload 相關用 `UPLOAD_*` / `MULTIPART_*`、與 OAuth 相關用 `OAUTH_*` / `MEMBER_CENTER_*` / `KNERON_CONVERTER_*`、與 FAA 相關用 `FILE_ACCESS_AGENT_*`、與 MinIO 相關用 `MINIO_*`、與 API key 認證相關用 `CONVERTER_API_KEY` +- **單位後綴**:時間 = `_MS` / `_SECONDS`;空間 = `_BYTES`;count = `_PER_<時間單位>` / `_MAX_COUNT`(不混用) +- **rate limit 命名**:`_PER_` 表單位時間最大次數;`_WINDOW_MS` 表 sliding window 大小;兩者成對出現(`_PER_10S` + `_BURST_WINDOW_MS=10000` 不算違規、單位來自 window) +- **bandwidth quota 命名**:`_QUOTA_PER__BYTES`,period 用單字(`HOUR` / `DAY`、非 `1H` / `24H`) +- **concurrent cap 命名**:`MAX_CONCURRENT_` 或 `_CONCURRENT_MAX`(本檔已存在 `MAX_CONCURRENT_UPLOADS` + `MAX_CONCURRENT_RESULT_STREAMS`、不再變動) + +### 16.4 已知非標準命名(接受、不要 rename) + +以下命名與 §16.3 規則不完全一致、但已散佈在 source code + tests + docs、rename 成本高、且語意清楚、**保留不變**: + +- `MAX_CONCURRENT_RESULT_STREAMS`(理論上應為 `RESULT_CONCURRENT_STREAM_MAX`、但 `MAX_CONCURRENT_UPLOADS` 已用此 pattern、保持一致) +- `MAX_CONCURRENT_UPLOADS`(同上) +- `KNERON_CONVERTER_CLIENT_ID` / `_SECRET`(理論上應為 `OAUTH_CONVERTER_*`、但這是 converter 在 OAuth 體系中的身份名、有歷史脈絡) +- `MEMBER_CENTER_TOKEN_URL`(service-name prefix 而非 scope prefix、但語意明確) + +未來新增 env 必依 §16.3、不再增加例外。 + +### 16.5 此節維護者責任 + +- **Architect**:拍板命名 + 維護本表;Backend 想新增 env 必先過此表(PR 改 `env.example` 時同步改本表) +- **Backend**:source code 增減 `process.env.*` 讀取點時,PR 必同步更新本表的 "Source code reads" 欄 +- **DevOps**:deployment / docker-compose env 注入以本表為唯一權威;不接受 source code 與本表不一致的部署設定 +- **Orchestrator**:派任務給 Backend / DevOps 時,env 命名必引用本表(不憑記憶寫) + +### 16.6 三方對齊現況(2026-05-18 deploy 時發現的不一致 + 解決狀態) + +| 不一致案例(deploy 時)| Orchestrator prompt 曾用過 | Source code 實際讀 | env.example §17 | 拍板 | +|----------------------|--------------------------|-------------------|-----------------|------| +| Sustained rate limit | `RESULT_RATE_LIMIT_SUSTAINED_MAX` | `RESULT_RATE_LIMIT_SUSTAINED_PER_MIN` + `_WINDOW_MS` | `RESULT_RATE_LIMIT_SUSTAINED_PER_MIN` + `_WINDOW_MS` | source code 為準(已對齊)| +| Bandwidth quota | `RESULT_BANDWIDTH_HOURLY_QUOTA` | `RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES` + `_PER_DAY_BYTES` | `RESULT_BANDWIDTH_QUOTA_PER_HOUR_BYTES` + `_PER_DAY_BYTES` | source code 為準(已對齊)| +| Concurrent cap | `RESULT_CONCURRENT_STREAM_MAX` | `MAX_CONCURRENT_RESULT_STREAMS` | `MAX_CONCURRENT_RESULT_STREAMS` | source code 為準(已對齊)| +| Stream timeout | `RESULT_STREAM_TIMEOUT_MS` | `RESULT_STREAM_TIMEOUT_MS` | `RESULT_STREAM_TIMEOUT_MS` | 三方原本就一致 | + +**結論**:實際 source code 與 `env.example` §17 已完全對齊(差異僅在 Orchestrator 派任務的 prompt 描述)。Backend 與 DevOps 不需動 source code / docker-compose / env.example;Orchestrator 後續派任務以本表為準即可。