✨ feat(vscode): 多人协作二期:状态分层/写校验/签名/团队视图

- 本机状态(会话id/worktree/profile/prDiffFile)迁 workspaceState,
  共享 state JSON 只留团队字段;读侧本地记录叠加,旧数据兜底
- mergeStateJsonComment 写后读回校验,并发覆盖时在最新状态上重放一次
- webhook 支持 Gitea secret,校验 X-Gitea-Signature(HMAC-SHA256),
  不匹配 401;设置面板新增 Webhook Secret 字段
- 看板新增 我的/全部 范围切换(团队视图),按工作区持久化
This commit is contained in:
cruldra committed 2026-08-19 00:32:06 +08:00
1 parent c370214d12
commit faf227412e
18 files changed
+665 -202

No files matched your search

+40 -1
View File
@@ -24,6 +24,8 @@
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
import type { Event } from 'vscode'
import { Buffer } from 'node:buffer'
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'
import { EventEmitter } from 'vscode'
import { logger } from '../logging/logger'
@@ -111,10 +113,33 @@ export interface PushWebhookEvent {
export class WebhookServer {
private server?: Server
private port?: number
private secret = ''
private readonly emitter = new EventEmitter<WebhookEvent>()
readonly onEvent: Event<WebhookEvent> = this.emitter.event
/**
* 设置(或清空)Gitea webhook secret。非空时对每个请求校验
* `X-Gitea-Signature`(HMAC-SHA256 over 原始 body 字节),不匹配一律 401
* ——端口经 frp 暴露公网,没有签名任何人都能伪造事件触发 codex 执行。
*/
setSecret(secret: string): void {
this.secret = secret
}
/** 时序安全的签名比对;头缺失/长度不符/非法 hex 都按不通过处理。 */
private verifySignature(rawBody: Buffer, signatureHeader: string): boolean {
const expected = createHmac('sha256', this.secret).update(rawBody).digest()
let received: Buffer
try {
received = Buffer.from(signatureHeader, 'hex')
}
catch {
return false
}
return received.length === expected.length && timingSafeEqual(received, expected)
}
/** The port currently bound, or undefined if the server is not listening. */
get currentPort(): number | undefined {
return this.port
@@ -220,7 +245,21 @@ export class WebhookServer {
req.on('data', (c: Buffer) => chunks.push(c))
req.on('end', () => {
try {
const body = Buffer.concat(chunks).toString('utf-8')
const rawBody = Buffer.concat(chunks)
if (this.secret) {
const signatureHeader = (req.headers['x-gitea-signature'] || '').toString()
if (!signatureHeader || !this.verifySignature(rawBody, signatureHeader)) {
logger.add({
level: 'warn',
source: 'webhook',
message: `签名校验失败,拒绝请求 (signature=${signatureHeader ? '不匹配' : '<缺失>'})`,
})
res.writeHead(401, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: false, error: 'invalid_signature' }))
return
}
}
const body = rawBody.toString('utf-8')
const eventHeader = (req.headers['x-gitea-event'] || '').toString() || '<missing>'
logger.add({
level: 'info',