koishi-plugin-github-webhook-pusher 0.0.7 → 0.0.9

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,188 @@
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
+ let payloadForVerify = rawBody;
47
+ if (!payloadForVerify) {
48
+ const requestBody = koaCtx.request?.body;
49
+ if (requestBody) {
50
+ // fallback: body parser 已消费流,无法获得 raw body
51
+ payloadForVerify = JSON.stringify(requestBody);
52
+ logger.warn('未获取到原始请求体,已使用解析后的 body 进行验签(可能导致验签失败)');
53
+ }
54
+ else {
55
+ logger.warn('无法获取请求体,无法进行签名验证');
56
+ koaCtx.status = 400;
57
+ koaCtx.body = { error: 'Missing request body' };
58
+ return;
59
+ }
60
+ }
61
+ if (!(0, signature_1.verifySignature)(payloadForVerify, signature, config.secret)) {
62
+ // 需求 1.3, 9.2: 签名验证失败
63
+ logger.warn('签名验证失败');
64
+ koaCtx.status = 401;
65
+ koaCtx.body = {
66
+ error: 'Invalid signature. Ensure Content-Type is application/json, and the signed payload matches the raw request body. 签名无效:请确认 Content-Type 为 application/json,且签名内容与实际请求体完全一致。'
67
+ };
68
+ return;
69
+ }
70
+ }
71
+ // 需求 1.5: 签名验证成功,继续处理
72
+ // 解析 JSON 负载
73
+ let payload;
74
+ if (rawBody) {
75
+ try {
76
+ payload = JSON.parse(rawBody);
77
+ }
78
+ catch (e) {
79
+ logger.warn('无法解析 JSON 负载');
80
+ koaCtx.status = 400;
81
+ koaCtx.body = { error: 'Invalid JSON payload' };
82
+ return;
83
+ }
84
+ }
85
+ else {
86
+ const requestBody = koaCtx.request?.body;
87
+ if (requestBody) {
88
+ payload = requestBody;
89
+ }
90
+ else {
91
+ logger.warn('请求体为空或无法读取');
92
+ koaCtx.status = 400;
93
+ koaCtx.body = { error: 'Missing request body' };
94
+ return;
95
+ }
96
+ }
97
+ // 获取仓库名
98
+ const repo = payload.repository?.full_name;
99
+ if (!repo) {
100
+ logger.warn('负载中缺少仓库信息');
101
+ koaCtx.status = 400;
102
+ koaCtx.body = { error: 'Missing repository information' };
103
+ return;
104
+ }
105
+ // 需求 9.1: 记录仓库名
106
+ logger.info(`收到 Webhook: [${repo}] ${eventName} (${deliveryId})`);
107
+ // 需求 1.6: 去重检查
108
+ if (deliveryId) {
109
+ const isDuplicate = await (0, repository_1.isDelivered)(ctx, deliveryId);
110
+ if (isDuplicate) {
111
+ logger.info(`跳过重复请求: ${deliveryId}`);
112
+ koaCtx.status = 200;
113
+ koaCtx.body = { status: 'duplicate' };
114
+ return;
115
+ }
116
+ }
117
+ // 需求 2.6: 信任仓库验证
118
+ if (!config.allowUntrusted) {
119
+ const trusted = await (0, repository_1.isTrusted)(ctx, repo);
120
+ if (!trusted) {
121
+ // 需求 9.3: 记录非信任仓库事件
122
+ logger.info(`忽略非信任仓库事件: ${repo}`);
123
+ koaCtx.status = 200;
124
+ koaCtx.body = { status: 'ignored', reason: 'untrusted' };
125
+ return;
126
+ }
127
+ }
128
+ // 解析事件
129
+ const event = (0, parser_1.parseEvent)(eventName, payload);
130
+ if (!event) {
131
+ logger.info(`不支持的事件类型: ${eventName}`);
132
+ koaCtx.status = 200;
133
+ koaCtx.body = { status: 'ignored', reason: 'unsupported' };
134
+ return;
135
+ }
136
+ // 需求 1.6: 记录投递(在处理前记录,防止重复处理)
137
+ if (deliveryId) {
138
+ await (0, repository_1.recordDelivery)(ctx, deliveryId, repo, eventName);
139
+ }
140
+ // 推送消息
141
+ const result = await (0, pusher_1.pushEvent)(ctx, event, config.concurrency);
142
+ // 需求 9.4: 记录推送结果
143
+ const elapsed = Date.now() - startTime;
144
+ logger.info(`处理完成: 推送 ${result.pushed} 成功, ${result.failed} 失败 (${elapsed}ms)`);
145
+ // 需求 1.5: 返回成功响应
146
+ koaCtx.status = 200;
147
+ koaCtx.body = { status: 'ok', pushed: result.pushed };
148
+ }
149
+ catch (error) {
150
+ // 需求 9.5: 记录错误
151
+ const errorMessage = error instanceof Error ? error.message : String(error);
152
+ logger.error(`处理 Webhook 时发生错误: ${errorMessage}`);
153
+ if (config.debug && error instanceof Error) {
154
+ logger.error(error.stack || '');
155
+ }
156
+ koaCtx.status = 500;
157
+ koaCtx.body = { status: 'error', error: 'Internal server error' };
158
+ }
159
+ });
160
+ }
161
+ /**
162
+ * 获取原始请求体
163
+ * @param koaCtx Koa 上下文
164
+ * @returns 原始请求体字符串
165
+ */
166
+ async function getRawBody(koaCtx) {
167
+ const rawBody = koaCtx.request?.rawBody;
168
+ if (typeof rawBody === 'string') {
169
+ return rawBody;
170
+ }
171
+ if (Buffer.isBuffer(rawBody)) {
172
+ return rawBody.toString('utf8');
173
+ }
174
+ if (!koaCtx.req?.readable) {
175
+ return null;
176
+ }
177
+ // 否则从流中读取
178
+ return new Promise((resolve, reject) => {
179
+ let data = '';
180
+ koaCtx.req.on('data', (chunk) => {
181
+ data += chunk.toString();
182
+ });
183
+ koaCtx.req.on('end', () => {
184
+ resolve(data);
185
+ });
186
+ koaCtx.req.on('error', reject);
187
+ });
188
+ }
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.7",
4
+ "version": "0.0.9",
5
5
  "contributors": [
6
6
  "ClozyA <aoxuan233@gmail.com>"
7
7
  ],
package/readme.md CHANGED
@@ -31,11 +31,12 @@ npm install koishi-plugin-github-webhook-pusher
31
31
  |--------|------|--------|------|
32
32
  | `path` | string | `/github/webhook` | Webhook 接收路径 |
33
33
  | `secret` | string | (必填) | GitHub Webhook Secret,用于签名验证 |
34
- | `baseUrl` | string | - | 显示用基础 URL |
35
34
  | `defaultEvents` | string[] | `['issues', 'release', 'push']` | 新订阅的默认事件类型 |
36
35
  | `debug` | boolean | `false` | 启用调试模式,输出详细日志 |
37
36
  | `allowUntrusted` | boolean | `false` | 是否允许处理非信任仓库的事件 |
38
37
  | `concurrency` | number | `5` | 消息推送并发数 |
38
+ | `deliveryRetentionDays` | number | `30` | 投递记录保留天数(<=0 表示不清理) |
39
+ | `deliveryCleanupIntervalHours` | number | `24` | 投递记录清理间隔(小时) |
39
40
 
40
41
  ### 配置示例
41
42
 
@@ -51,6 +52,8 @@ plugins:
51
52
  debug: false
52
53
  allowUntrusted: false
53
54
  concurrency: 5
55
+ deliveryRetentionDays: 30
56
+ deliveryCleanupIntervalHours: 24
54
57
  ```
55
58
 
56
59
  ## GitHub Webhook 设置指南
@@ -178,6 +181,7 @@ https://github.com/owner/repo/releases/tag/v1.0.0
178
181
  **A:** 签名验证失败。请检查:
179
182
  1. GitHub Webhook 设置中的 Secret 与插件配置的 `secret` 是否一致
180
183
  2. Content type 是否设置为 `application/json`
184
+ 3. 如果服务器未保留 raw body,插件会使用解析后的 body 回退验签,可能导致签名不一致;建议配置保留原始请求体
181
185
 
182
186
  ### Q: 收不到 Webhook 事件?
183
187