- commit
- 4c63a12
- parent
- 458d7cf
- author
- im_wower
- date
- 2026-03-22 01:24:17 +0800 CST
Merge remote-tracking branch 'origin/feat/T-018-control-api-deploy' into integration/fourth-wave-20260322
12 files changed,
+254,
-43
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
+3,
-0
1@@ -0,0 +1,3 @@
2+.dev.vars*
3+!.dev.vars.example
4+.wrangler/
+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 }
+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 {
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 }
+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
+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+}
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
+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 时会直接拒绝请求,而不是静默降级为匿名访问。
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 "$@"
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
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 "$@"