koishi-plugin-github-webhook-pusher 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/webhook.js ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook 处理器
4
+ * 需求: 1.1-1.6, 2.6, 9.1-9.6
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.registerWebhook = registerWebhook;
8
+ const koishi_1 = require("koishi");
9
+ const signature_1 = require("./signature");
10
+ const parser_1 = require("./parser");
11
+ const pusher_1 = require("./pusher");
12
+ const repository_1 = require("./repository");
13
+ const logger = new koishi_1.Logger('github-webhook');
14
+ /**
15
+ * 注册 Webhook 处理器
16
+ * 需求 1.1: 在配置的路径上监听 HTTP POST 请求
17
+ * @param ctx Koishi 上下文
18
+ * @param config 插件配置
19
+ */
20
+ function registerWebhook(ctx, config) {
21
+ const path = config.path || '/github/webhook';
22
+ logger.info(`注册 Webhook 处理器: ${path}`);
23
+ // 使用 ctx.server.post 注册 HTTP POST 路由
24
+ ctx.server.post(path, async (koaCtx) => {
25
+ const startTime = Date.now();
26
+ try {
27
+ // 获取请求信息
28
+ const signature = koaCtx.get('X-Hub-Signature-256');
29
+ const eventName = koaCtx.get('X-GitHub-Event');
30
+ const deliveryId = koaCtx.get('X-GitHub-Delivery');
31
+ // 获取原始请求体
32
+ const rawBody = await getRawBody(koaCtx);
33
+ // 需求 9.1: 记录请求来源、事件类型
34
+ if (config.debug) {
35
+ logger.debug(`收到 Webhook 请求: event=${eventName}, delivery=${deliveryId}`);
36
+ }
37
+ // 需求 1.2, 1.3, 1.4: 签名验证
38
+ if (config.secret) {
39
+ if (!signature) {
40
+ // 需求 1.4: 缺少签名头
41
+ logger.warn('请求缺少 X-Hub-Signature-256 头');
42
+ koaCtx.status = 401;
43
+ koaCtx.body = { error: 'Missing signature' };
44
+ return;
45
+ }
46
+ if (!(0, signature_1.verifySignature)(rawBody, signature, config.secret)) {
47
+ // 需求 1.3, 9.2: 签名验证失败
48
+ logger.warn('签名验证失败');
49
+ koaCtx.status = 401;
50
+ koaCtx.body = {
51
+ error: 'Invalid signature. Ensure Content-Type is application/json, and the signed payload matches the raw request body. 签名无效:请确认 Content-Type 为 application/json,且签名内容与实际请求体完全一致。'
52
+ };
53
+ return;
54
+ }
55
+ }
56
+ // 需求 1.5: 签名验证成功,继续处理
57
+ // 解析 JSON 负载
58
+ let payload;
59
+ try {
60
+ payload = JSON.parse(rawBody);
61
+ }
62
+ catch (e) {
63
+ logger.warn('无法解析 JSON 负载');
64
+ koaCtx.status = 400;
65
+ koaCtx.body = { error: 'Invalid JSON payload' };
66
+ return;
67
+ }
68
+ // 获取仓库名
69
+ const repo = payload.repository?.full_name;
70
+ if (!repo) {
71
+ logger.warn('负载中缺少仓库信息');
72
+ koaCtx.status = 400;
73
+ koaCtx.body = { error: 'Missing repository information' };
74
+ return;
75
+ }
76
+ // 需求 9.1: 记录仓库名
77
+ logger.info(`收到 Webhook: [${repo}] ${eventName} (${deliveryId})`);
78
+ // 需求 1.6: 去重检查
79
+ if (deliveryId) {
80
+ const isDuplicate = await (0, repository_1.isDelivered)(ctx, deliveryId);
81
+ if (isDuplicate) {
82
+ logger.info(`跳过重复请求: ${deliveryId}`);
83
+ koaCtx.status = 200;
84
+ koaCtx.body = { status: 'duplicate' };
85
+ return;
86
+ }
87
+ }
88
+ // 需求 2.6: 信任仓库验证
89
+ if (!config.allowUntrusted) {
90
+ const trusted = await (0, repository_1.isTrusted)(ctx, repo);
91
+ if (!trusted) {
92
+ // 需求 9.3: 记录非信任仓库事件
93
+ logger.info(`忽略非信任仓库事件: ${repo}`);
94
+ koaCtx.status = 200;
95
+ koaCtx.body = { status: 'ignored', reason: 'untrusted' };
96
+ return;
97
+ }
98
+ }
99
+ // 解析事件
100
+ const event = (0, parser_1.parseEvent)(eventName, payload);
101
+ if (!event) {
102
+ logger.info(`不支持的事件类型: ${eventName}`);
103
+ koaCtx.status = 200;
104
+ koaCtx.body = { status: 'ignored', reason: 'unsupported' };
105
+ return;
106
+ }
107
+ // 需求 1.6: 记录投递(在处理前记录,防止重复处理)
108
+ if (deliveryId) {
109
+ await (0, repository_1.recordDelivery)(ctx, deliveryId, repo, eventName);
110
+ }
111
+ // 推送消息
112
+ const result = await (0, pusher_1.pushEvent)(ctx, event, config.concurrency);
113
+ // 需求 9.4: 记录推送结果
114
+ const elapsed = Date.now() - startTime;
115
+ logger.info(`处理完成: 推送 ${result.pushed} 成功, ${result.failed} 失败 (${elapsed}ms)`);
116
+ // 需求 1.5: 返回成功响应
117
+ koaCtx.status = 200;
118
+ koaCtx.body = { status: 'ok', pushed: result.pushed };
119
+ }
120
+ catch (error) {
121
+ // 需求 9.5: 记录错误
122
+ const errorMessage = error instanceof Error ? error.message : String(error);
123
+ logger.error(`处理 Webhook 时发生错误: ${errorMessage}`);
124
+ if (config.debug && error instanceof Error) {
125
+ logger.error(error.stack || '');
126
+ }
127
+ koaCtx.status = 500;
128
+ koaCtx.body = { status: 'error', error: 'Internal server error' };
129
+ }
130
+ });
131
+ }
132
+ /**
133
+ * 获取原始请求体
134
+ * @param koaCtx Koa 上下文
135
+ * @returns 原始请求体字符串
136
+ */
137
+ async function getRawBody(koaCtx) {
138
+ // 如果已经有解析好的 body,尝试重新序列化
139
+ if (koaCtx.request.body) {
140
+ return JSON.stringify(koaCtx.request.body);
141
+ }
142
+ // 否则从流中读取
143
+ return new Promise((resolve, reject) => {
144
+ let data = '';
145
+ koaCtx.req.on('data', (chunk) => {
146
+ data += chunk.toString();
147
+ });
148
+ koaCtx.req.on('end', () => {
149
+ resolve(data);
150
+ });
151
+ koaCtx.req.on('error', reject);
152
+ });
153
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-github-webhook-pusher",
3
3
  "description": "GitHub Webhook 事件推送插件",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "contributors": [
6
6
  "ClozyA <aoxuan233@gmail.com>"
7
7
  ],
@@ -19,7 +19,10 @@
19
19
  "license": "MIT",
20
20
  "scripts": {
21
21
  "test": "vitest --run",
22
- "test:watch": "vitest"
22
+ "test:watch": "vitest",
23
+ "build": "tsc -p tsconfig.json",
24
+ "build:ci": "tsc -p tsconfig.github.json",
25
+ "prepublishOnly": "npm run build:ci"
23
26
  },
24
27
  "keywords": [
25
28
  "chatbot",