feat(worker): complete Fetch web console support#26
Conversation
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Walkthrough本次改动为 Web Console 引入基于 HMAC 签名的会话 Cookie 机制,并将 Web Console 请求处理重构为 Fetch API 风格(Request/Response),同时在 Cloudflare Worker 路由中接入 Web Console 处理逻辑并区分 Telegram webhook 路径,附带相应测试与计划文档、配置日期更新。 ChangesWeb Console 会话与 Fetch 化路由
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-07-01-workers-completion.md`:
- Line 321: The plan document’s SessionKind examples and self-review are
inconsistent with the implemented contract, which uses "password" instead of
"admin". Update the SessionKind references in the affected Task examples and the
Self-Review to match the actual types used by web-console-session.ts and the
related test signatures, ensuring all documented session kind values are
"password" | "setup" and no "admin" remains.
In `@src/runtime/web-console-session.ts`:
- Line 22: The session cookie built in web-console-session’s cookie setter is
missing the Secure attribute for production use. Update the Set-Cookie string
construction in the session cookie helper so `inboxbridge_session` is marked
`Secure` by default in HTTPS/Worker environments, while keeping any HTTP/local
override logic at the call site if needed; use the existing cookie assembly in
`web-console-session` as the single place to add this flag.
In `@src/runtime/web-console.ts`:
- Around line 376-397: POST /login 的失败分支没有任何限流,导致无效登录请求可以被无限重试。请在 web-console
的登录处理逻辑中,围绕 loginSessionKind / renderLogin 这条失败路径加入按
IP、用户名或会话维度的失败计数与冷却/封禁机制,并在超过阈值时直接拒绝而不是继续渲染登录页;同时保持成功登录时清理或重置对应计数,确保 public
console 不易被暴力尝试。
- Around line 469-477: `readNodeRequestBody` currently buffers the entire
request before any size check, so large POST bodies can exhaust memory before
`readRequestForm` enforces `maxFormBodyBytes`. Update `readNodeRequestBody` to
track accumulated bytes inside the `for await` loop and throw
`FormBodyTooLargeError` as soon as the limit is exceeded, keeping the check
synchronous with streaming reads; use the existing `readNodeRequestBody` and
`readRequestForm` flow as the integration points.
In `@src/runtime/worker.ts`:
- Around line 67-68: The current WEB_CONSOLE_SESSION_SECRET check in the worker
only verifies presence, so weak values like “test” or “123” can still be
accepted. Update the validation in the worker’s session secret handling to
enforce a minimum strength requirement, such as requiring a 32-byte random
secret (or equivalent entropy) before continuing, and keep returning a 503 from
the same json error path when the secret is missing or too weak. Use the
existing WEB_CONSOLE_SESSION_SECRET gate in src/runtime/worker.ts as the place
to add the stronger validation.
- Around line 98-102: Worker 控制台操作现在用
listConversations/listFailedDeliveries/listAuditLogs/searchMessages
返回空结果、scheduleRetry 也只是 no-op,导致 /operations/deliveries/retry
之类入口会显示成功但没有任何数据库写入。请在 src/runtime/worker.ts 中基于现有的 Worker 操作实现补上 D1 版本回调(尤其是
scheduleRetry 和各个 list* 方法),确保这些方法真实读写数据库;如果暂时无法实现,则在 Worker
的控制台入口里禁用或隐藏这些未实现的操作,避免返回假成功。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9fe7b00f-73ae-4378-b940-df800aaa475f
📒 Files selected for processing (6)
docs/superpowers/plans/2026-07-01-workers-completion.mdsrc/runtime/web-console-session.tssrc/runtime/web-console.tssrc/runtime/worker.tstest/core.test.tswrangler.toml
| Create `src/runtime/web-console-session.ts`: | ||
|
|
||
| ```ts | ||
| export type SessionKind = "setup" | "admin"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
计划文档中的 SessionKind 取值与实际实现不一致。
本计划文档在 Task 3 的示例代码(SessionKind = "setup" | "admin",第 321 行)以及 Task 1/2/5 的测试片段中反复使用 "admin" 作为会话种类;但根据本次改动引入的实际实现(web-console-session.ts 与 test/core.test.ts 中新增的签名 Cookie 测试均使用 kind: "password"),真实的 SessionKind/WebConsoleSessionKind 取值应为 "password" | "setup"。第 723 行的 Self-Review 甚至断言“Session kind remains "setup" | "admin"”,这与实际交付的代码契约不符,未来若有人依据本计划继续实施后续任务(Task 6/7 或衍生工作),可能被误导。
建议将文档中所有 "admin" 更新为 "password",并修正第 723 行的自查结论。
📝 建议修正
- Type consistency: Session kind remains `"setup" | "admin"`; Fetch handler remains `handleWebConsoleRequest(request, options, sessions?)`; Worker entry remains `handleWorkerFetch(request, env, options?)`.
+ Type consistency: Session kind remains `"setup" | "password"`; Fetch handler remains `handleWebConsoleRequest(request, options, sessions?)`; Worker entry remains `handleWorkerFetch(request, env, options?)`.Also applies to: 354-354, 723-723
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-07-01-workers-completion.md` at line 321, The
plan document’s SessionKind examples and self-review are inconsistent with the
implemented contract, which uses "password" instead of "admin". Update the
SessionKind references in the affected Task examples and the Self-Review to
match the actual types used by web-console-session.ts and the related test
signatures, ensuring all documented session kind values are "password" | "setup"
and no "admin" remains.
| const expiresAt = Math.floor(input.now.getTime() / 1000) + input.maxAgeSeconds; | ||
| const payload = base64UrlEncode(new TextEncoder().encode(JSON.stringify({ kind: input.kind, exp: expiresAt }))); | ||
| const signature = await sign(input.secret, payload); | ||
| return `${cookieName}=${payload}.${signature}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${input.maxAgeSeconds}`; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
为会话 Cookie 增加生产环境 Secure 属性。
inboxbridge_session 是控制台认证凭据;当前 Set-Cookie 未带 Secure,生产/Worker HTTPS 场景下应避免 Cookie 被明文 HTTP 请求携带。若仍需支持本地 HTTP,可在调用层按协议配置,但 Worker/生产默认应启用。
建议修复方向
- return `${cookieName}=${payload}.${signature}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${input.maxAgeSeconds}`;
+ return `${cookieName}=${payload}.${signature}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${input.maxAgeSeconds}`;
...
- return `${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+ return `${cookieName}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`;Also applies to: 44-45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/web-console-session.ts` at line 22, The session cookie built in
web-console-session’s cookie setter is missing the Secure attribute for
production use. Update the Set-Cookie string construction in the session cookie
helper so `inboxbridge_session` is marked `Secure` by default in HTTPS/Worker
environments, while keeping any HTTP/local override logic at the call site if
needed; use the existing cookie assembly in `web-console-session` as the single
place to add this flag.
| if (url.pathname === "/login" && request.method === "POST") { | ||
| const form = await readRequestForm(request); | ||
| const sessionKind = await loginSessionKind(options.settings, form); | ||
| if (sessionKind) { | ||
| const serverResponse = res.asServerResponse(); | ||
| if (options.sessionSecret) { | ||
| serverResponse.setHeader("set-cookie", await createSignedSessionCookie({ | ||
| secret: options.sessionSecret, | ||
| kind: sessionKind, | ||
| now: currentDate(options), | ||
| maxAgeSeconds: options.sessionMaxAgeSeconds ?? 60 * 60 * 8, | ||
| })); | ||
| } else { | ||
| const session = randomBytes(24).toString("hex"); | ||
| sessions.set(session, sessionKind); | ||
| serverResponse.setHeader("set-cookie", `${sessionCookie}=${session}; HttpOnly; SameSite=Lax; Path=/`); | ||
| } | ||
| redirect(serverResponse, "/"); | ||
| return res.toResponse(); | ||
| } | ||
| await renderLogin(res.asServerResponse(), options.settings, "登录凭据无效。"); | ||
| return res.toResponse(); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
登录失败路径需要限流。
POST /login 对无效凭据直接重新渲染,没有按 IP/账号/会话做失败次数限制;公开部署控制台时会放大密码或 setup token 暴力尝试风险。As per path instructions, src/**/*.ts: “重点审查 ... 限流 ... 和用户数据泄露的问题”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/web-console.ts` around lines 376 - 397, POST /login
的失败分支没有任何限流,导致无效登录请求可以被无限重试。请在 web-console 的登录处理逻辑中,围绕 loginSessionKind /
renderLogin 这条失败路径加入按
IP、用户名或会话维度的失败计数与冷却/封禁机制,并在超过阈值时直接拒绝而不是继续渲染登录页;同时保持成功登录时清理或重置对应计数,确保 public
console 不易被暴力尝试。
Source: Path instructions
| const body = method === "GET" || method === "HEAD" ? undefined : await readNodeRequestBody(req); | ||
| return new Request(url, { method, headers, body }); | ||
| } | ||
|
|
||
| async function readNodeRequestBody(req: IncomingMessage): Promise<ArrayBuffer> { | ||
| const chunks: Buffer[] = []; | ||
| for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
| const body = Buffer.concat(chunks); | ||
| return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
读取 Node 请求体时同步限制大小。
这里先完整 Buffer.concat,readRequestForm 才检查 maxFormBodyBytes;攻击者可向 /login 或认证 POST 发送超大 body,在触发 413 前耗尽内存。请在 for await 循环中累计字节并提前抛出 FormBodyTooLargeError。
建议修复
async function readNodeRequestBody(req: IncomingMessage): Promise<ArrayBuffer> {
const chunks: Buffer[] = [];
- for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+ let size = 0;
+ for await (const chunk of req) {
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
+ size += buffer.length;
+ if (size > maxFormBodyBytes) throw new FormBodyTooLargeError("form body too large");
+ chunks.push(buffer);
+ }
const body = Buffer.concat(chunks);
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const body = method === "GET" || method === "HEAD" ? undefined : await readNodeRequestBody(req); | |
| return new Request(url, { method, headers, body }); | |
| } | |
| async function readNodeRequestBody(req: IncomingMessage): Promise<ArrayBuffer> { | |
| const chunks: Buffer[] = []; | |
| for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | |
| const body = Buffer.concat(chunks); | |
| return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer; | |
| const body = method === "GET" || method === "HEAD" ? undefined : await readNodeRequestBody(req); | |
| return new Request(url, { method, headers, body }); | |
| } | |
| async function readNodeRequestBody(req: IncomingMessage): Promise<ArrayBuffer> { | |
| const chunks: Buffer[] = []; | |
| let size = 0; | |
| for await (const chunk of req) { | |
| const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); | |
| size += buffer.length; | |
| if (size > maxFormBodyBytes) throw new FormBodyTooLargeError("form body too large"); | |
| chunks.push(buffer); | |
| } | |
| const body = Buffer.concat(chunks); | |
| return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/web-console.ts` around lines 469 - 477, `readNodeRequestBody`
currently buffers the entire request before any size check, so large POST bodies
can exhaust memory before `readRequestForm` enforces `maxFormBodyBytes`. Update
`readNodeRequestBody` to track accumulated bytes inside the `for await` loop and
throw `FormBodyTooLargeError` as soon as the limit is exceeded, keeping the
check synchronous with streaming reads; use the existing `readNodeRequestBody`
and `readRequestForm` flow as the integration points.
| const sessionSecret = typeof env.WEB_CONSOLE_SESSION_SECRET === "string" ? env.WEB_CONSOLE_SESSION_SECRET : ""; | ||
| if (!sessionSecret) return json({ error: "WEB_CONSOLE_SESSION_SECRET is required" }, { status: 503 }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
校验 WEB_CONSOLE_SESSION_SECRET 强度。
仅检查存在会接受 "test"/"123" 这类低熵密钥;攻击者一旦猜中 HMAC secret 即可伪造 password 会话 Cookie。建议要求至少 32 字节随机值,并在不满足时返回 503。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/worker.ts` around lines 67 - 68, The current
WEB_CONSOLE_SESSION_SECRET check in the worker only verifies presence, so weak
values like “test” or “123” can still be accepted. Update the validation in the
worker’s session secret handling to enforce a minimum strength requirement, such
as requiring a 32-byte random secret (or equivalent entropy) before continuing,
and keep returning a 503 from the same json error path when the secret is
missing or too weak. Use the existing WEB_CONSOLE_SESSION_SECRET gate in
src/runtime/worker.ts as the place to add the stronger validation.
| listConversations: () => ({ items: [], total: 0 }), | ||
| listFailedDeliveries: () => ({ items: [], total: 0 }), | ||
| scheduleRetry: async () => {}, | ||
| listAuditLogs: () => ({ items: [], total: 0 }), | ||
| searchMessages: () => ({ items: [], total: 0 }), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
不要让 Worker 控制台操作返回假成功。
list* 全部返回空数据且 scheduleRetry 是 no-op,但 /operations/deliveries/retry 会重定向到成功状态;管理员会看到“已重试”但数据库没有任何写入。请实现 D1 版本回调,或在 Worker 中禁用/隐藏未实现的操作入口。As per path instructions, src/**/*.ts: “重点审查 ... 数据库写入逻辑”.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime/worker.ts` around lines 98 - 102, Worker 控制台操作现在用
listConversations/listFailedDeliveries/listAuditLogs/searchMessages
返回空结果、scheduleRetry 也只是 no-op,导致 /operations/deliveries/retry
之类入口会显示成功但没有任何数据库写入。请在 src/runtime/worker.ts 中基于现有的 Worker 操作实现补上 D1 版本回调(尤其是
scheduleRetry 和各个 list* 方法),确保这些方法真实读写数据库;如果暂时无法实现,则在 Worker
的控制台入口里禁用或隐藏这些未实现的操作,避免返回假成功。
Source: Path instructions
Summary
Validation
Cloudflare token status
Wiki
Summary by CodeRabbit
新功能
Bug 修复
文档