baa-conductor

git clone 

commit
d2b625d
parent
bbded8a
author
im_wower
date
2026-03-22 12:27:16 +0800 CST
Simplify control API auth for temporary mini setup
10 files changed,  +86, -65
M apps/control-api-worker/.dev.vars.example
+10, -7
 1@@ -1,10 +1,13 @@
 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-CONTROL_API_AUTH_REQUIRED=true
 6-BAA_SHARED_TOKEN=replace-me
 7-CONTROL_API_BROWSER_ADMIN_TOKEN=replace-me
 8-CONTROL_API_CONTROLLER_TOKEN=replace-me
 9-CONTROL_API_OPS_ADMIN_TOKEN=replace-me
10-CONTROL_API_READONLY_TOKEN=replace-me
11-CONTROL_API_WORKER_TOKEN=replace-me
12+CONTROL_API_AUTH_REQUIRED=false
13+# Temporary single-node mode currently runs without Control API auth.
14+# If you later want to re-enable auth, set CONTROL_API_AUTH_REQUIRED=true
15+# and provide the tokens below.
16+# BAA_SHARED_TOKEN=replace-me
17+# CONTROL_API_BROWSER_ADMIN_TOKEN=replace-me
18+# CONTROL_API_CONTROLLER_TOKEN=replace-me
19+# CONTROL_API_OPS_ADMIN_TOKEN=replace-me
20+# CONTROL_API_READONLY_TOKEN=replace-me
21+# CONTROL_API_WORKER_TOKEN=replace-me
M apps/control-api-worker/local/harness.mjs
+15, -9
 1@@ -103,8 +103,14 @@ export async function createLocalControlApiHarness(options = {}) {
 2 }
 3 
 4 export async function runMinimalControlApiSmoke(options = {}) {
 5-  const { databasePath = ":memory:", log = () => {}, resetDatabase = true } = options;
 6+  const {
 7+    databasePath = ":memory:",
 8+    log = () => {},
 9+    requestWithoutToken = false,
10+    resetDatabase = true
11+  } = options;
12   const harness = options.harness ?? (await createLocalControlApiHarness({
13+    authRequired: options.authRequired,
14     databasePath,
15     resetDatabase
16   }));
17@@ -114,7 +120,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
18     const initialState = await harness.request({
19       method: "GET",
20       path: "/v1/system/state",
21-      token: harness.tokens.readonly
22+      token: requestWithoutToken ? undefined : harness.tokens.readonly
23     });
24     assert.equal(initialState.status, 200);
25     assert.equal(initialState.json?.data?.mode, "running");
26@@ -123,7 +129,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
27     const pauseResult = await harness.request({
28       method: "POST",
29       path: "/v1/system/pause",
30-      token: harness.tokens.browserAdmin,
31+      token: requestWithoutToken ? undefined : harness.tokens.browserAdmin,
32       body: {
33         reason: "local smoke",
34         requested_by: "local-smoke"
35@@ -136,7 +142,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
36     const pausedState = await harness.request({
37       method: "GET",
38       path: "/v1/system/state",
39-      token: harness.tokens.readonly
40+      token: requestWithoutToken ? undefined : harness.tokens.readonly
41     });
42     assert.equal(pausedState.status, 200);
43     assert.equal(pausedState.json?.data?.mode, "paused");
44@@ -145,7 +151,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
45     const heartbeatResult = await harness.request({
46       method: "POST",
47       path: "/v1/controllers/heartbeat",
48-      token: harness.tokens.controller,
49+      token: requestWithoutToken ? undefined : harness.tokens.controller,
50       body: {
51         controller_id: "mini-local",
52         host: "127.0.0.1",
53@@ -164,7 +170,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
54     const leaderLease = await harness.request({
55       method: "POST",
56       path: "/v1/leader/acquire",
57-      token: harness.tokens.controller,
58+      token: requestWithoutToken ? undefined : harness.tokens.controller,
59       body: {
60         controller_id: "mini-local",
61         host: "127.0.0.1",
62@@ -180,7 +186,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
63     const leasedState = await harness.request({
64       method: "GET",
65       path: "/v1/system/state",
66-      token: harness.tokens.readonly
67+      token: requestWithoutToken ? undefined : harness.tokens.readonly
68     });
69     assert.equal(leasedState.status, 200);
70     assert.equal(leasedState.json?.data?.holder_id, "mini-local");
71@@ -190,7 +196,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
72     const createdTask = await harness.request({
73       method: "POST",
74       path: "/v1/tasks",
75-      token: harness.tokens.browserAdmin,
76+      token: requestWithoutToken ? undefined : harness.tokens.browserAdmin,
77       body: {
78         acceptance: ["minimal smoke closes a read/write loop"],
79         goal: "Verify local control-api D1 smoke",
80@@ -210,7 +216,7 @@ export async function runMinimalControlApiSmoke(options = {}) {
81     const readTask = await harness.request({
82       method: "GET",
83       path: `/v1/tasks/${taskId}`,
84-      token: harness.tokens.readonly
85+      token: requestWithoutToken ? undefined : harness.tokens.readonly
86     });
87     assert.equal(readTask.status, 200);
88     assert.equal(readTask.json?.data?.task_id, taskId);
M apps/control-api-worker/src/runtime.ts
+17, -3
 1@@ -61,9 +61,13 @@ export function createControlApiAuthHook(
 2   tokenVerifier?: AuthTokenVerifier
 3 ): ControlApiRequestAuthHook | null {
 4   const hasEnvTokens = hasConfiguredEnvTokens(env);
 5-  const authRequired = parseBooleanEnv(env[CONTROL_API_AUTH_REQUIRED_ENV_NAME]) ?? false;
 6+  const authRequired = parseBooleanEnv(env[CONTROL_API_AUTH_REQUIRED_ENV_NAME]);
 7 
 8-  if (!tokenVerifier && !hasEnvTokens && !authRequired) {
 9+  if (authRequired === false) {
10+    return null;
11+  }
12+
13+  if (!tokenVerifier && !hasEnvTokens && authRequired !== true) {
14     return null;
15   }
16 
17@@ -223,7 +227,17 @@ function parseBooleanEnv(value: string | undefined): boolean | undefined {
18     return undefined;
19   }
20 
21-  return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
22+  const normalized = value.trim().toLowerCase();
23+
24+  if (TRUE_ENV_VALUES.has(normalized)) {
25+    return true;
26+  }
27+
28+  if (["0", "false", "no", "off"].includes(normalized)) {
29+    return false;
30+  }
31+
32+  return undefined;
33 }
34 
35 function resolveServiceRole(action: string): "controller" | "worker" | null {
M apps/control-api-worker/wrangler.jsonc
+3, -3
 1@@ -11,14 +11,14 @@
 2   ],
 3   "vars": {
 4     "CONTROL_API_VERSION": "2026-03-22",
 5-    "CONTROL_API_AUTH_REQUIRED": "true"
 6+    "CONTROL_API_AUTH_REQUIRED": "false"
 7   },
 8   "d1_databases": [
 9     {
10       "binding": "CONTROL_DB",
11       "database_name": "baa-conductor-control-prod",
12-      "database_id": "00000000-0000-0000-0000-000000000000",
13-      "preview_database_id": "00000000-0000-0000-0000-000000000000",
14+      "database_id": "e2cf2bce-a744-478c-bdcd-b159aa0645c0",
15+      "preview_database_id": "e2cf2bce-a744-478c-bdcd-b159aa0645c0",
16       "migrations_table": "d1_migrations",
17       "migrations_dir": "../../ops/sql/migrations"
18     }
M docs/auth/README.md
+8, -0
 1@@ -2,6 +2,14 @@
 2 
 3 `T-012` 的目标不是直接把鉴权做成生产实现,而是先把 Control API 周围的角色、Token 形式和授权边界压成一套可复用的模型,供后续 `T-003` 接入。
 4 
 5+当前仓库的临时单节点运行模式已经简化为:
 6+
 7+- `mini` 单节点
 8+- Firefox 插件默认直连 `https://control-api.makefile.so`
 9+- `CONTROL_API_AUTH_REQUIRED=false`
10+
11+也就是说,这份文档现在更偏“鉴权模型预案”,不是当前 live 临时部署的硬要求。
12+
13 ## 设计原则
14 
15 - 所有 Control API 请求统一走 `Authorization: Bearer <token>`
M docs/firefox/README.md
+8, -12
 1@@ -36,23 +36,19 @@
 2 
 3 插件文案必须和这个语义保持一致,尤其不要把 `paused` 展示成“所有运行中的工作都已强制停止”。
 4 
 5-## 3. 鉴权与入口
 6+## 3. 入口与当前简化模式
 7 
 8 推荐入口:
 9 
10 - Base URL: `https://control-api.makefile.so`
11 
12-请求头:
13+当前单节点临时模式:
14 
15-- `Authorization: Bearer <token>`
16-- `Content-Type: application/json`
17+- `CONTROL_API_AUTH_REQUIRED=false`
18+- Firefox 插件默认直接请求 `https://control-api.makefile.so`
19+- 当前不要求在插件里填写 token
20 
21-角色约束:
22-
23-- `browser_admin`: 可以调用 `GET /v1/system/state`、`POST /v1/system/pause`、`POST /v1/system/resume`、`POST /v1/system/drain`
24-- `readonly`: 只可读取 `GET /v1/system/state`
25-
26-如果插件同时需要读写,直接使用 `browser_admin` token 即可。
27+如果后续重新启用鉴权,再恢复 `Authorization: Bearer <token>` 即可。
28 
29 ## 4. 读取状态
30 
31@@ -192,7 +188,7 @@ Firefox 插件至少要读取这些字段:
32 
33 插件至少要处理这些错误:
34 
35-- `401` / `403`: token 无效或角色不足
36+- `401` / `403`: 如果后续重新启用鉴权,则表示 token 无效或角色不足
37 - `409`: 非法状态迁移或并发冲突
38 - `5xx`: control API 暂时不可用
39 
40@@ -251,5 +247,5 @@ Firefox 插件要明确区分两个通道:
41 - 实现 `POST /v1/system/pause`
42 - 实现 `POST /v1/system/resume`
43 - 可选实现 `POST /v1/system/drain`
44-- 所有写操作都携带 `browser_admin` token
45+- 当前临时模式下,插件不携带 token;如果后续重新启用鉴权,再恢复 bearer token
46 - 所有状态展示都以 control API 返回值为准
M plugins/baa-firefox/controller.html
+1, -3
 1@@ -27,9 +27,7 @@
 2       <input id="ws-url" type="text" spellcheck="false">
 3       <button id="save-ws-btn" type="button">保存</button>
 4       <label for="control-base-url">控制 API</label>
 5-      <input id="control-base-url" type="text" spellcheck="false" placeholder="http://127.0.0.1:9800">
 6-      <label for="control-token">令牌</label>
 7-      <input id="control-token" type="password" spellcheck="false" placeholder="browser_session token">
 8+      <input id="control-base-url" type="text" spellcheck="false" placeholder="https://control-api.makefile.so">
 9       <button id="save-control-btn" type="button">保存控制面配置</button>
10       <button id="refresh-control-btn" type="button">刷新状态</button>
11       <button id="pause-btn" type="button">暂停</button>
M plugins/baa-firefox/controller.js
+7, -22
  1@@ -9,7 +9,6 @@ const CONTROLLER_STORAGE_KEYS = {
  2   clientId: "baaFirefox.clientId",
  3   wsUrl: "baaFirefox.wsUrl",
  4   controlBaseUrl: "baaFirefox.controlBaseUrl",
  5-  controlToken: "baaFirefox.controlToken",
  6   controlState: "baaFirefox.controlState",
  7   trackedTabs: "baaFirefox.trackedTabs",
  8   endpointsByPlatform: "baaFirefox.endpointsByPlatform",
  9@@ -19,6 +18,7 @@ const CONTROLLER_STORAGE_KEYS = {
 10 };
 11 
 12 const DEFAULT_WS_URL = "ws://127.0.0.1:9800";
 13+const DEFAULT_CONTROL_BASE_URL = "https://control-api.makefile.so";
 14 const CREDENTIAL_SEND_INTERVAL = 30_000;
 15 const NETWORK_BODY_LIMIT = 5000;
 16 const LOG_LIMIT = 160;
 17@@ -136,8 +136,7 @@ const pendingProxyRequests = new Map();
 18 const state = {
 19   clientId: null,
 20   wsUrl: DEFAULT_WS_URL,
 21-  controlBaseUrl: "",
 22-  controlToken: "",
 23+  controlBaseUrl: DEFAULT_CONTROL_BASE_URL,
 24   controlState: null,
 25   ws: null,
 26   wsConnected: false,
 27@@ -170,9 +169,10 @@ function deriveControlBaseUrl(wsUrl) {
 28   try {
 29     const parsed = new URL(wsUrl || DEFAULT_WS_URL);
 30     parsed.protocol = parsed.protocol === "wss:" ? "https:" : "http:";
 31-    return trimTrailingSlash(parsed.toString());
 32+    const derived = trimTrailingSlash(parsed.toString());
 33+    return derived === "http://127.0.0.1:9800" ? DEFAULT_CONTROL_BASE_URL : derived;
 34   } catch (_) {
 35-    return "http://127.0.0.1:9800";
 36+    return DEFAULT_CONTROL_BASE_URL;
 37   }
 38 }
 39 
 40@@ -683,7 +683,6 @@ async function persistState() {
 41     [CONTROLLER_STORAGE_KEYS.clientId]: state.clientId,
 42     [CONTROLLER_STORAGE_KEYS.wsUrl]: state.wsUrl,
 43     [CONTROLLER_STORAGE_KEYS.controlBaseUrl]: state.controlBaseUrl,
 44-    [CONTROLLER_STORAGE_KEYS.controlToken]: state.controlToken,
 45     [CONTROLLER_STORAGE_KEYS.controlState]: state.controlState,
 46     [CONTROLLER_STORAGE_KEYS.trackedTabs]: state.trackedTabs,
 47     [CONTROLLER_STORAGE_KEYS.endpointsByPlatform]: state.endpoints,
 48@@ -830,10 +829,6 @@ async function requestControlPlane(path, options = {}) {
 49     Accept: "application/json"
 50   });
 51 
 52-  if (state.controlToken) {
 53-    headers.set("Authorization", `Bearer ${state.controlToken}`);
 54-  }
 55-
 56   if (options.body != null && !headers.has("Content-Type")) {
 57     headers.set("Content-Type", "application/json");
 58   }
 59@@ -1643,7 +1638,6 @@ function bindUi() {
 60   ui.logView = qs("log-view");
 61   ui.wsUrl = qs("ws-url");
 62   ui.controlBaseUrl = qs("control-base-url");
 63-  ui.controlToken = qs("control-token");
 64 
 65   for (const platform of PLATFORM_ORDER) {
 66     qs(`open-${platform}-btn`).addEventListener("click", () => {
 67@@ -1660,19 +1654,14 @@ function bindUi() {
 68   });
 69 
 70   qs("save-ws-btn").addEventListener("click", () => {
 71-    const previousDerived = deriveControlBaseUrl(state.wsUrl);
 72     state.wsUrl = ui.wsUrl.value.trim() || DEFAULT_WS_URL;
 73-    if (!ui.controlBaseUrl.value.trim() || trimTrailingSlash(ui.controlBaseUrl.value) === previousDerived) {
 74-      state.controlBaseUrl = deriveControlBaseUrl(state.wsUrl);
 75-    }
 76     persistState().catch(() => {});
 77     addLog("info", `已保存 WS 地址:${state.wsUrl}`, false);
 78     render();
 79   });
 80 
 81   qs("save-control-btn").addEventListener("click", () => {
 82-    state.controlBaseUrl = trimTrailingSlash(ui.controlBaseUrl.value) || deriveControlBaseUrl(state.wsUrl);
 83-    state.controlToken = ui.controlToken.value.trim();
 84+    state.controlBaseUrl = trimTrailingSlash(ui.controlBaseUrl.value) || DEFAULT_CONTROL_BASE_URL;
 85     persistState().catch(() => {});
 86     addLog("info", `已保存控制 API:${state.controlBaseUrl}`, false);
 87     render();
 88@@ -1713,10 +1702,7 @@ async function init() {
 89   if (state.wsUrl === "ws://localhost:9800") {
 90     state.wsUrl = DEFAULT_WS_URL;
 91   }
 92-  state.controlBaseUrl = trimTrailingSlash(saved[CONTROLLER_STORAGE_KEYS.controlBaseUrl]) || deriveControlBaseUrl(state.wsUrl);
 93-  state.controlToken = typeof saved[CONTROLLER_STORAGE_KEYS.controlToken] === "string"
 94-    ? saved[CONTROLLER_STORAGE_KEYS.controlToken]
 95-    : "";
 96+  state.controlBaseUrl = trimTrailingSlash(saved[CONTROLLER_STORAGE_KEYS.controlBaseUrl]) || DEFAULT_CONTROL_BASE_URL;
 97   state.controlState = loadControlState(saved[CONTROLLER_STORAGE_KEYS.controlState]);
 98 
 99   state.trackedTabs = loadTrackedTabs(
100@@ -1740,7 +1726,6 @@ async function init() {
101 
102   ui.wsUrl.value = state.wsUrl;
103   ui.controlBaseUrl.value = state.controlBaseUrl;
104-  ui.controlToken.value = state.controlToken;
105 
106   registerRuntimeListeners();
107   registerTabListeners();
M plugins/baa-firefox/docs/conductor-control.md
+6, -6
 1@@ -5,7 +5,7 @@
 2 ## 范围
 3 
 4 - `controller.html` / `controller.js`
 5-  - 配置 Control API base URL 和 bearer token
 6+  - 配置 Control API base URL
 7   - 读取 `GET /v1/system/state`
 8   - 调用 `POST /v1/system/pause`
 9   - 调用 `POST /v1/system/resume`
10@@ -17,12 +17,12 @@
11 
12 ## 配置
13 
14-控制页新增了两项:
15+控制页新增了一项:
16 
17 - `Control API`
18-  - 默认从当前 `wsUrl` 推导,例如 `ws://127.0.0.1:9800` -> `http://127.0.0.1:9800`
19-- `Bearer`
20-  - 直接写入 `Authorization: Bearer <token>`
21+  - 默认预填 `https://control-api.makefile.so`
22+
23+当前临时单节点模式默认不开启 Control API 鉴权,因此插件不会再要求填写 bearer token。
24 
25 状态快照会持久化到 `browser.storage.local` 的 `baaFirefox.controlState`,供:
26 
27@@ -55,4 +55,4 @@
28 
29 - 本次没有改 `manifest.json`
30 - 因此 Control API 需要落在当前扩展 CSP 允许的地址范围内
31-- 最稳妥的做法是把 conductor control API 暴露在与当前 `wsUrl` 同源的本地桥接地址上
32+- 当前默认目标就是 `https://control-api.makefile.so`
M tests/control-api/control-api-smoke.test.mjs
+11, -0
 1@@ -12,3 +12,14 @@ test("control-api smoke completes a minimal local D1 read/write loop", async ()
 2   assert.match(summary.task_id, /^task_/u);
 3   assert.ok(summary.term >= 1);
 4 });
 5+
 6+test("control-api allows unauthenticated requests when auth is explicitly disabled", async () => {
 7+  const summary = await runMinimalControlApiSmoke({
 8+    authRequired: false,
 9+    requestWithoutToken: true
10+  });
11+
12+  assert.equal(summary.mode, "paused");
13+  assert.equal(summary.holder_id, "mini-local");
14+  assert.equal(summary.task_status, "queued");
15+});