- 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
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
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);
+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 {
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 }
+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>`
+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 返回值为准
+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>
+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();
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`
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+});