baa-conductor

git clone 

commit
4022431
parent
458d7cf
author
im_wower
date
2026-03-22 01:03:22 +0800 CST
feat(control-api-worker): add cloudflare deployment template
12 files changed,  +254, -43
A apps/control-api-worker/.dev.vars.example
+9, -0
 1@@ -0,0 +1,9 @@
 2+# Copy to .dev.vars or .dev.vars.production for local wrangler dev.
 3+# Keep the deployed Worker secrets in Cloudflare; do not commit real values.
 4+
 5+BAA_SHARED_TOKEN=replace-me
 6+CONTROL_API_BROWSER_ADMIN_TOKEN=replace-me
 7+CONTROL_API_CONTROLLER_TOKEN=replace-me
 8+CONTROL_API_OPS_ADMIN_TOKEN=replace-me
 9+CONTROL_API_READONLY_TOKEN=replace-me
10+CONTROL_API_WORKER_TOKEN=replace-me
A apps/control-api-worker/.gitignore
+3, -0
1@@ -0,0 +1,3 @@
2+.dev.vars*
3+!.dev.vars.example
4+.wrangler/
M apps/control-api-worker/package.json
+4, -1
 1@@ -5,6 +5,9 @@
 2   "main": "dist/index.js",
 3   "scripts": {
 4     "build": "pnpm exec tsc -p tsconfig.json && BAA_DIST_DIR=apps/control-api-worker/dist BAA_DIST_ENTRY=apps/control-api-worker/src/index.js BAA_IMPORT_ALIASES='@baa-conductor/auth=../../../packages/auth/src/index.js;@baa-conductor/db=../../../packages/db/src/index.js' BAA_FIX_RELATIVE_EXTENSIONS=true BAA_EXPORT_DEFAULT=true pnpm -C ../.. run build:runtime-postprocess",
 5-    "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json"
 6+    "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json",
 7+    "deploy:cloudflare": "bash ../../ops/cloudflare/deploy-control-api-worker.sh",
 8+    "migrate:d1:remote": "bash ../../ops/cloudflare/apply-control-api-d1-migrations.sh",
 9+    "tail:cloudflare": "npx --yes wrangler@4 tail --config wrangler.jsonc"
10   }
11 }
M apps/control-api-worker/src/contracts.ts
+33, -9
 1@@ -6,6 +6,30 @@ import type {
 2 } from "@baa-conductor/auth";
 3 import type { ControlPlaneRepository, D1DatabaseLike, JsonValue } from "@baa-conductor/db";
 4 
 5+export const CONTROL_API_WORKER_NAME = "baa-conductor-control-api" as const;
 6+export const CONTROL_API_WORKER_ENTRY = "dist/index.js" as const;
 7+export const CONTROL_API_CUSTOM_DOMAIN = "control-api.makefile.so" as const;
 8+export const CONTROL_API_D1_BINDING_NAME = "CONTROL_DB" as const;
 9+export const CONTROL_API_D1_DATABASE_NAME = "baa-conductor-control-prod" as const;
10+export const CONTROL_API_VERSION_ENV_NAME = "CONTROL_API_VERSION" as const;
11+export const CONTROL_API_AUTH_REQUIRED_ENV_NAME = "CONTROL_API_AUTH_REQUIRED" as const;
12+export const CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME =
13+  "CONTROL_API_BROWSER_ADMIN_TOKEN" as const;
14+export const CONTROL_API_CONTROLLER_TOKEN_ENV_NAME = "CONTROL_API_CONTROLLER_TOKEN" as const;
15+export const CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME = "CONTROL_API_OPS_ADMIN_TOKEN" as const;
16+export const CONTROL_API_READONLY_TOKEN_ENV_NAME = "CONTROL_API_READONLY_TOKEN" as const;
17+export const CONTROL_API_WORKER_TOKEN_ENV_NAME = "CONTROL_API_WORKER_TOKEN" as const;
18+export const BAA_SHARED_TOKEN_ENV_NAME = "BAA_SHARED_TOKEN" as const;
19+
20+export const CONTROL_API_SECRET_ENV_NAMES = [
21+  BAA_SHARED_TOKEN_ENV_NAME,
22+  CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME,
23+  CONTROL_API_CONTROLLER_TOKEN_ENV_NAME,
24+  CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME,
25+  CONTROL_API_READONLY_TOKEN_ENV_NAME,
26+  CONTROL_API_WORKER_TOKEN_ENV_NAME
27+] as const;
28+
29 export type ControlApiRouteMethod = "GET" | "POST";
30 
31 export type ControlApiRouteId =
32@@ -33,15 +57,15 @@ export interface ControlApiRouteSchemaDescriptor {
33 }
34 
35 export interface ControlApiEnv {
36-  CONTROL_DB?: D1DatabaseLike;
37-  CONTROL_API_VERSION?: string;
38-  CONTROL_API_AUTH_REQUIRED?: string;
39-  CONTROL_API_BROWSER_ADMIN_TOKEN?: string;
40-  CONTROL_API_CONTROLLER_TOKEN?: string;
41-  CONTROL_API_OPS_ADMIN_TOKEN?: string;
42-  CONTROL_API_READONLY_TOKEN?: string;
43-  CONTROL_API_WORKER_TOKEN?: string;
44-  BAA_SHARED_TOKEN?: string;
45+  [CONTROL_API_D1_BINDING_NAME]?: D1DatabaseLike;
46+  [CONTROL_API_VERSION_ENV_NAME]?: string;
47+  [CONTROL_API_AUTH_REQUIRED_ENV_NAME]?: string;
48+  [CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME]?: string;
49+  [CONTROL_API_CONTROLLER_TOKEN_ENV_NAME]?: string;
50+  [CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME]?: string;
51+  [CONTROL_API_READONLY_TOKEN_ENV_NAME]?: string;
52+  [CONTROL_API_WORKER_TOKEN_ENV_NAME]?: string;
53+  [BAA_SHARED_TOKEN_ENV_NAME]?: string;
54 }
55 
56 export interface ControlApiExecutionContext {
M apps/control-api-worker/src/handlers.ts
+3, -4
 1@@ -15,9 +15,8 @@ import type {
 2   ControlApiRouteHandler,
 3   ControlApiRouteMethod
 4 } from "./contracts.js";
 5-import {
 6-  CONTROL_API_ROUTE_SCHEMAS
 7-} from "./schemas.js";
 8+import { CONTROL_API_D1_BINDING_NAME } from "./contracts.js";
 9+import { CONTROL_API_ROUTE_SCHEMAS } from "./schemas.js";
10 
11 const DEFAULT_TASK_PRIORITY = 50;
12 
13@@ -253,7 +252,7 @@ function buildRepositoryNotConfiguredFailure(context: ControlApiRouteContext): C
14     ok: false,
15     status: 503,
16     error: "repository_not_configured",
17-    message: `Route ${context.route.id} requires CONTROL_DB or an injected repository.`,
18+    message: `Route ${context.route.id} requires ${CONTROL_API_D1_BINDING_NAME} or an injected repository.`,
19     details: {
20       route_id: context.route.id
21     }
M apps/control-api-worker/src/runtime.ts
+32, -22
  1@@ -8,13 +8,21 @@ import {
  2   DEFAULT_AUTH_AUDIENCE
  3 } from "@baa-conductor/auth";
  4 import { createD1ControlPlaneRepository } from "@baa-conductor/db";
  5-import type {
  6-  ControlApiEnv,
  7-  ControlApiHandlerFailure,
  8-  ControlApiRequestAuthHook,
  9-  ControlApiRouteAuthorization,
 10-  ControlApiServices,
 11-  ControlApiWorkerOptions
 12+import {
 13+  BAA_SHARED_TOKEN_ENV_NAME,
 14+  CONTROL_API_AUTH_REQUIRED_ENV_NAME,
 15+  CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME,
 16+  CONTROL_API_CONTROLLER_TOKEN_ENV_NAME,
 17+  CONTROL_API_D1_BINDING_NAME,
 18+  CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME,
 19+  CONTROL_API_READONLY_TOKEN_ENV_NAME,
 20+  CONTROL_API_WORKER_TOKEN_ENV_NAME,
 21+  type ControlApiEnv,
 22+  type ControlApiHandlerFailure,
 23+  type ControlApiRequestAuthHook,
 24+  type ControlApiRouteAuthorization,
 25+  type ControlApiServices,
 26+  type ControlApiWorkerOptions
 27 } from "./contracts.js";
 28 
 29 const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
 30@@ -38,12 +46,14 @@ function resolveRepository(
 31     return options.repository;
 32   }
 33 
 34-  if (!env.CONTROL_DB) {
 35+  const database = env[CONTROL_API_D1_BINDING_NAME];
 36+
 37+  if (!database) {
 38     return null;
 39   }
 40 
 41   const repositoryFactory = options.repositoryFactory ?? createD1ControlPlaneRepository;
 42-  return repositoryFactory(env.CONTROL_DB);
 43+  return repositoryFactory(database);
 44 }
 45 
 46 export function createControlApiAuthHook(
 47@@ -51,7 +61,7 @@ export function createControlApiAuthHook(
 48   tokenVerifier?: AuthTokenVerifier
 49 ): ControlApiRequestAuthHook | null {
 50   const hasEnvTokens = hasConfiguredEnvTokens(env);
 51-  const authRequired = parseBooleanEnv(env.CONTROL_API_AUTH_REQUIRED) ?? false;
 52+  const authRequired = parseBooleanEnv(env[CONTROL_API_AUTH_REQUIRED_ENV_NAME]) ?? false;
 53 
 54   if (!tokenVerifier && !hasEnvTokens && !authRequired) {
 55     return null;
 56@@ -144,42 +154,42 @@ function verifyEnvToken(
 57   resource: AuthResourceOwnership | undefined,
 58   env: ControlApiEnv
 59 ): AuthVerificationResult {
 60-  if (token === env.CONTROL_API_BROWSER_ADMIN_TOKEN) {
 61+  if (token === env[CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME]) {
 62     return {
 63       ok: true,
 64       principal: buildStaticPrincipal("browser_admin")
 65     };
 66   }
 67 
 68-  if (token === env.CONTROL_API_READONLY_TOKEN) {
 69+  if (token === env[CONTROL_API_READONLY_TOKEN_ENV_NAME]) {
 70     return {
 71       ok: true,
 72       principal: buildStaticPrincipal("readonly")
 73     };
 74   }
 75 
 76-  if (token === env.CONTROL_API_OPS_ADMIN_TOKEN) {
 77+  if (token === env[CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME]) {
 78     return {
 79       ok: true,
 80       principal: buildStaticPrincipal("ops_admin")
 81     };
 82   }
 83 
 84-  if (token === env.CONTROL_API_CONTROLLER_TOKEN) {
 85+  if (token === env[CONTROL_API_CONTROLLER_TOKEN_ENV_NAME]) {
 86     return {
 87       ok: true,
 88       principal: buildServicePrincipal("controller", resource)
 89     };
 90   }
 91 
 92-  if (token === env.CONTROL_API_WORKER_TOKEN) {
 93+  if (token === env[CONTROL_API_WORKER_TOKEN_ENV_NAME]) {
 94     return {
 95       ok: true,
 96       principal: buildServicePrincipal("worker", resource)
 97     };
 98   }
 99 
100-  if (token === env.BAA_SHARED_TOKEN) {
101+  if (token === env[BAA_SHARED_TOKEN_ENV_NAME]) {
102     const role = resolveServiceRole(action);
103 
104     if (role) {
105@@ -199,12 +209,12 @@ function verifyEnvToken(
106 
107 function hasConfiguredEnvTokens(env: ControlApiEnv): boolean {
108   return [
109-    env.BAA_SHARED_TOKEN,
110-    env.CONTROL_API_BROWSER_ADMIN_TOKEN,
111-    env.CONTROL_API_CONTROLLER_TOKEN,
112-    env.CONTROL_API_OPS_ADMIN_TOKEN,
113-    env.CONTROL_API_READONLY_TOKEN,
114-    env.CONTROL_API_WORKER_TOKEN
115+    env[BAA_SHARED_TOKEN_ENV_NAME],
116+    env[CONTROL_API_BROWSER_ADMIN_TOKEN_ENV_NAME],
117+    env[CONTROL_API_CONTROLLER_TOKEN_ENV_NAME],
118+    env[CONTROL_API_OPS_ADMIN_TOKEN_ENV_NAME],
119+    env[CONTROL_API_READONLY_TOKEN_ENV_NAME],
120+    env[CONTROL_API_WORKER_TOKEN_ENV_NAME]
121   ].some((value) => typeof value === "string" && value.trim().length > 0);
122 }
123 
A apps/control-api-worker/wrangler.jsonc
+29, -0
 1@@ -0,0 +1,29 @@
 2+{
 3+  "name": "baa-conductor-control-api",
 4+  "main": "dist/index.js",
 5+  "compatibility_date": "2026-03-22",
 6+  "workers_dev": false,
 7+  "routes": [
 8+    {
 9+      "pattern": "control-api.makefile.so",
10+      "custom_domain": true
11+    }
12+  ],
13+  "vars": {
14+    "CONTROL_API_VERSION": "2026-03-22",
15+    "CONTROL_API_AUTH_REQUIRED": "true"
16+  },
17+  "d1_databases": [
18+    {
19+      "binding": "CONTROL_DB",
20+      "database_name": "baa-conductor-control-prod",
21+      "database_id": "00000000-0000-0000-0000-000000000000",
22+      "preview_database_id": "00000000-0000-0000-0000-000000000000",
23+      "migrations_table": "d1_migrations",
24+      "migrations_dir": "../../ops/sql/migrations"
25+    }
26+  ],
27+  "observability": {
28+    "enabled": true
29+  }
30+}
M coordination/tasks/T-018-control-api-deploy.md
+28, -7
 1@@ -1,10 +1,10 @@
 2 ---
 3 task_id: T-018
 4 title: Cloudflare Worker 与 D1 部署配置
 5-status: todo
 6+status: review
 7 branch: feat/T-018-control-api-deploy
 8 repo: /Users/george/code/baa-conductor
 9-base_ref: main
10+base_ref: main@458d7cf
11 depends_on:
12   - T-014
13 write_scope:
14@@ -64,23 +64,44 @@ updated_at: 2026-03-22
15 
16 ## files_changed
17 
18-- 待填写
19+- `apps/control-api-worker/.dev.vars.example`
20+- `apps/control-api-worker/.gitignore`
21+- `apps/control-api-worker/package.json`
22+- `apps/control-api-worker/src/contracts.ts`
23+- `apps/control-api-worker/src/handlers.ts`
24+- `apps/control-api-worker/src/runtime.ts`
25+- `apps/control-api-worker/wrangler.jsonc`
26+- `ops/cloudflare/README.md`
27+- `ops/cloudflare/apply-control-api-d1-migrations.sh`
28+- `ops/cloudflare/control-api-worker.secrets.example.env`
29+- `ops/cloudflare/deploy-control-api-worker.sh`
30+- `coordination/tasks/T-018-control-api-deploy.md`
31 
32 ## commands_run
33 
34-- 待填写
35+- `git worktree add /Users/george/code/baa-conductor-T018 -b feat/T-018-control-api-deploy 458d7cf`
36+- `npx --yes pnpm install`
37+- `bash -n ops/cloudflare/deploy-control-api-worker.sh ops/cloudflare/apply-control-api-d1-migrations.sh`
38+- `npx --yes pnpm --filter @baa-conductor/control-api-worker typecheck`
39+- `npx --yes pnpm --filter @baa-conductor/control-api-worker build`
40 
41 ## result
42 
43-- 待填写
44+- 为 `control-api-worker` 增加了可提交的 `wrangler.jsonc`,固定了 Worker 名称、入口 `dist/index.js`、自定义域 `control-api.makefile.so`、D1 binding `CONTROL_DB` 和最小运行时变量。
45+- 在 `contracts.ts` / `runtime.ts` / `handlers.ts` 中把 D1 binding 与 token/env 名称抽成常量并接入运行时,减少代码和部署配置之间的漂移。
46+- 在 `ops/cloudflare/` 下补了最小运维材料:部署说明、secret 模板、远端 D1 migration 脚本、构建后部署脚本;同时在包脚本中暴露了 Cloudflare 相关入口。
47 
48 ## risks
49 
50-- 待填写
51+- `apps/control-api-worker/wrangler.jsonc` 中的 `database_id` 与 `preview_database_id` 仍是占位值;实际部署前必须替换为真实 D1 UUID。
52+- 本任务只提供 secret 模板和 `wrangler secret put` 约定,真实 secret 仍需在目标 Cloudflare 账号中手动注入。
53+- 未执行真实 Cloudflare deploy / D1 远端 migration,因此自定义域、账号权限和线上资源绑定仍需落地验证。
54 
55 ## next_handoff
56 
57-- 待填写
58+- 先填写 `wrangler.jsonc` 的真实 D1 UUID,再执行 `./ops/cloudflare/apply-control-api-d1-migrations.sh`。
59+- 按 `ops/cloudflare/control-api-worker.secrets.example.env` 的键名把生产 secret 写入 Cloudflare Worker。
60+- 完成后执行 `./ops/cloudflare/deploy-control-api-worker.sh`,并验证 `https://control-api.makefile.so` 是否能正常返回 Worker 响应。
61 
62 开始时建议直接把 `status` 改为 `in_progress`。
63 
A ops/cloudflare/README.md
+55, -0
 1@@ -0,0 +1,55 @@
 2+# Cloudflare Worker / D1 操作约定
 3+
 4+本目录只负责 `apps/control-api-worker` 的最小部署骨架,不包含真实 Cloudflare 账号信息。
 5+
 6+## 绑定与入口
 7+
 8+- Worker 配置文件:`apps/control-api-worker/wrangler.jsonc`
 9+- Worker 名称:`baa-conductor-control-api`
10+- 入口文件:`dist/index.js`
11+- 自定义域:`control-api.makefile.so`
12+- D1 binding:`CONTROL_DB`
13+- D1 数据库名模板:`baa-conductor-control-prod`
14+
15+当前 Worker 代码和部署配置统一使用以下运行时变量:
16+
17+- 普通变量:`CONTROL_API_VERSION`、`CONTROL_API_AUTH_REQUIRED`
18+- Secret:`BAA_SHARED_TOKEN`、`CONTROL_API_BROWSER_ADMIN_TOKEN`、`CONTROL_API_CONTROLLER_TOKEN`、`CONTROL_API_OPS_ADMIN_TOKEN`、`CONTROL_API_READONLY_TOKEN`、`CONTROL_API_WORKER_TOKEN`
19+
20+## 首次部署前
21+
22+1. 把 `apps/control-api-worker/wrangler.jsonc` 中的 `database_id` / `preview_database_id` 替换成真实 D1 UUID。
23+2. 以 `control-api-worker.secrets.example.env` 为模板,准备不入库的 secret 清单。
24+3. 确保本机已配置 `CLOUDFLARE_API_TOKEN`,或者已通过 `wrangler login` 完成鉴权。
25+
26+## 推荐命令
27+
28+应用远端 D1 migrations:
29+
30+```bash
31+./ops/cloudflare/apply-control-api-d1-migrations.sh
32+```
33+
34+构建并部署 Worker:
35+
36+```bash
37+./ops/cloudflare/deploy-control-api-worker.sh
38+```
39+
40+查看线上日志:
41+
42+```bash
43+cd apps/control-api-worker
44+npx --yes wrangler@4 tail --config wrangler.jsonc
45+```
46+
47+## Secret 下发
48+
49+建议按 `control-api-worker.secrets.example.env` 的键名逐个执行:
50+
51+```bash
52+cd apps/control-api-worker
53+printf '%s' 'replace-me' | npx --yes wrangler@4 secret put BAA_SHARED_TOKEN --config wrangler.jsonc
54+```
55+
56+其余 `CONTROL_API_*_TOKEN` secret 同理。`CONTROL_API_AUTH_REQUIRED` 已在 `wrangler.jsonc` 里固定为 `"true"`,因此生产环境缺失 secret 时会直接拒绝请求,而不是静默降级为匿名访问。
A ops/cloudflare/apply-control-api-d1-migrations.sh
+25, -0
 1@@ -0,0 +1,25 @@
 2+#!/usr/bin/env bash
 3+set -euo pipefail
 4+
 5+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 6+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
 7+APP_DIR="${REPO_ROOT}/apps/control-api-worker"
 8+CONFIG_PATH="${APP_DIR}/wrangler.jsonc"
 9+
10+if [[ ! -f "${CONFIG_PATH}" ]]; then
11+  echo "wrangler config not found: ${CONFIG_PATH}" >&2
12+  exit 1
13+fi
14+
15+DEFAULT_DATABASE_NAME="$(sed -n 's/.*"database_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "${CONFIG_PATH}" | head -n 1)"
16+DATABASE_NAME="${1:-${DEFAULT_DATABASE_NAME}}"
17+
18+if [[ -z "${DATABASE_NAME}" ]]; then
19+  echo "Unable to determine database_name from ${CONFIG_PATH}." >&2
20+  exit 1
21+fi
22+
23+shift $(( $# > 0 ? 1 : 0 ))
24+
25+cd "${APP_DIR}"
26+npx --yes wrangler@4 d1 migrations apply "${DATABASE_NAME}" --config wrangler.jsonc --remote "$@"
A ops/cloudflare/control-api-worker.secrets.example.env
+9, -0
 1@@ -0,0 +1,9 @@
 2+# Copy this file outside the repo, fill in real values, then push each key with:
 3+# printf '%s' "$VALUE" | npx --yes wrangler@4 secret put <KEY> --config apps/control-api-worker/wrangler.jsonc
 4+
 5+BAA_SHARED_TOKEN=replace-me
 6+CONTROL_API_BROWSER_ADMIN_TOKEN=replace-me
 7+CONTROL_API_CONTROLLER_TOKEN=replace-me
 8+CONTROL_API_OPS_ADMIN_TOKEN=replace-me
 9+CONTROL_API_READONLY_TOKEN=replace-me
10+CONTROL_API_WORKER_TOKEN=replace-me
A ops/cloudflare/deploy-control-api-worker.sh
+24, -0
 1@@ -0,0 +1,24 @@
 2+#!/usr/bin/env bash
 3+set -euo pipefail
 4+
 5+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 6+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
 7+APP_DIR="${REPO_ROOT}/apps/control-api-worker"
 8+CONFIG_PATH="${APP_DIR}/wrangler.jsonc"
 9+PLACEHOLDER_DATABASE_ID="00000000-0000-0000-0000-000000000000"
10+
11+if [[ ! -f "${CONFIG_PATH}" ]]; then
12+  echo "wrangler config not found: ${CONFIG_PATH}" >&2
13+  exit 1
14+fi
15+
16+if rg --fixed-strings --quiet "${PLACEHOLDER_DATABASE_ID}" "${CONFIG_PATH}"; then
17+  echo "Replace database_id / preview_database_id in ${CONFIG_PATH} before deploying." >&2
18+  exit 1
19+fi
20+
21+cd "${REPO_ROOT}"
22+npx --yes pnpm --filter @baa-conductor/control-api-worker build
23+
24+cd "${APP_DIR}"
25+npx --yes wrangler@4 deploy --config wrangler.jsonc "$@"