@onebots/core 1.0.0 → 1.0.4

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/metrics.js ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 性能指标收集系统
3
+ * 收集请求数、响应时间、错误率等指标
4
+ */
5
+ import { createLogger } from './logger.js';
6
+ const logger = createLogger('Metrics');
7
+ /**
8
+ * 指标收集器
9
+ */
10
+ export class MetricsCollector {
11
+ metrics = new Map();
12
+ maxSamples = 1000; // 每个指标最多保留的样本数
13
+ /**
14
+ * 增加计数器
15
+ */
16
+ increment(name, value = 1, labels) {
17
+ const metric = this.getOrCreateMetric(name, 'counter', labels);
18
+ const lastValue = metric.values[metric.values.length - 1];
19
+ const newValue = (lastValue?.value || 0) + value;
20
+ metric.values.push({
21
+ value: newValue,
22
+ timestamp: Date.now(),
23
+ labels,
24
+ });
25
+ this.trimSamples(metric);
26
+ }
27
+ /**
28
+ * 设置仪表值
29
+ */
30
+ set(name, value, labels) {
31
+ const metric = this.getOrCreateMetric(name, 'gauge', labels);
32
+ metric.values.push({
33
+ value,
34
+ timestamp: Date.now(),
35
+ labels,
36
+ });
37
+ this.trimSamples(metric);
38
+ }
39
+ /**
40
+ * 记录直方图值
41
+ */
42
+ observe(name, value, labels) {
43
+ const metric = this.getOrCreateMetric(name, 'histogram', labels);
44
+ metric.values.push({
45
+ value,
46
+ timestamp: Date.now(),
47
+ labels,
48
+ });
49
+ this.trimSamples(metric);
50
+ }
51
+ /**
52
+ * 获取或创建指标
53
+ */
54
+ getOrCreateMetric(name, type, labels) {
55
+ const key = this.getMetricKey(name, labels);
56
+ if (!this.metrics.has(key)) {
57
+ this.metrics.set(key, {
58
+ name,
59
+ type,
60
+ help: `${name} metric`,
61
+ values: [],
62
+ labels,
63
+ });
64
+ }
65
+ return this.metrics.get(key);
66
+ }
67
+ /**
68
+ * 获取指标键
69
+ */
70
+ getMetricKey(name, labels) {
71
+ if (!labels || Object.keys(labels).length === 0) {
72
+ return name;
73
+ }
74
+ const labelStr = Object.entries(labels)
75
+ .sort(([a], [b]) => a.localeCompare(b))
76
+ .map(([k, v]) => `${k}="${v}"`)
77
+ .join(',');
78
+ return `${name}{${labelStr}}`;
79
+ }
80
+ /**
81
+ * 修剪样本数量
82
+ */
83
+ trimSamples(metric) {
84
+ if (metric.values.length > this.maxSamples) {
85
+ metric.values = metric.values.slice(-this.maxSamples);
86
+ }
87
+ }
88
+ /**
89
+ * 获取指标
90
+ */
91
+ getMetric(name, labels) {
92
+ const key = this.getMetricKey(name, labels);
93
+ return this.metrics.get(key);
94
+ }
95
+ /**
96
+ * 获取所有指标
97
+ */
98
+ getAllMetrics() {
99
+ return Array.from(this.metrics.values());
100
+ }
101
+ /**
102
+ * 获取最新值
103
+ */
104
+ getLatestValue(name, labels) {
105
+ const metric = this.getMetric(name, labels);
106
+ if (!metric || metric.values.length === 0) {
107
+ return undefined;
108
+ }
109
+ return metric.values[metric.values.length - 1].value;
110
+ }
111
+ /**
112
+ * 计算平均值
113
+ */
114
+ getAverage(name, labels, windowMs) {
115
+ const metric = this.getMetric(name, labels);
116
+ if (!metric || metric.values.length === 0) {
117
+ return undefined;
118
+ }
119
+ const now = Date.now();
120
+ const cutoff = windowMs ? now - windowMs : 0;
121
+ const relevantValues = metric.values.filter(v => v.timestamp >= cutoff);
122
+ if (relevantValues.length === 0) {
123
+ return undefined;
124
+ }
125
+ const sum = relevantValues.reduce((acc, v) => acc + v.value, 0);
126
+ return sum / relevantValues.length;
127
+ }
128
+ /**
129
+ * 计算总和
130
+ */
131
+ getSum(name, labels, windowMs) {
132
+ const metric = this.getMetric(name, labels);
133
+ if (!metric || metric.values.length === 0) {
134
+ return undefined;
135
+ }
136
+ const now = Date.now();
137
+ const cutoff = windowMs ? now - windowMs : 0;
138
+ const relevantValues = metric.values.filter(v => v.timestamp >= cutoff);
139
+ if (relevantValues.length === 0) {
140
+ return undefined;
141
+ }
142
+ return relevantValues.reduce((acc, v) => acc + v.value, 0);
143
+ }
144
+ /**
145
+ * 导出为 Prometheus 格式
146
+ */
147
+ exportPrometheus() {
148
+ const lines = [];
149
+ const processed = new Set();
150
+ for (const metric of this.metrics.values()) {
151
+ const baseKey = metric.name;
152
+ if (processed.has(baseKey)) {
153
+ continue;
154
+ }
155
+ processed.add(baseKey);
156
+ // 添加 HELP 和 TYPE
157
+ lines.push(`# HELP ${baseKey} ${metric.help}`);
158
+ lines.push(`# TYPE ${baseKey} ${metric.type}`);
159
+ // 添加所有相同名称的指标值
160
+ for (const m of this.metrics.values()) {
161
+ if (m.name === baseKey) {
162
+ const labelStr = m.labels && Object.keys(m.labels).length > 0
163
+ ? `{${Object.entries(m.labels).map(([k, v]) => `${k}="${v}"`).join(',')}}`
164
+ : '';
165
+ const latestValue = m.values[m.values.length - 1];
166
+ if (latestValue) {
167
+ lines.push(`${baseKey}${labelStr} ${latestValue.value}`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return lines.join('\n') + '\n';
173
+ }
174
+ /**
175
+ * 清理过期数据
176
+ */
177
+ cleanup(maxAge = 3600000) {
178
+ const now = Date.now();
179
+ const cutoff = now - maxAge;
180
+ for (const metric of this.metrics.values()) {
181
+ metric.values = metric.values.filter(v => v.timestamp >= cutoff);
182
+ // 如果指标没有值了,删除它
183
+ if (metric.values.length === 0) {
184
+ this.metrics.delete(this.getMetricKey(metric.name, metric.labels));
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * 重置所有指标
190
+ */
191
+ reset() {
192
+ this.metrics.clear();
193
+ logger.info('All metrics reset');
194
+ }
195
+ }
196
+ // 全局指标收集器实例
197
+ export const metrics = new MetricsCollector();
198
+ // 定期清理过期数据(每 10 分钟)
199
+ setInterval(() => {
200
+ metrics.cleanup();
201
+ }, 10 * 60 * 1000);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 中间件统一导出
3
+ */
4
+ export { createRateLimit, defaultRateLimit } from './rate-limit.js';
5
+ export { initSecurityAudit, securityAudit, logAuthFailure, logInvalidToken, logSuspiciousRequest, logRateLimit, closeSecurityAudit, } from './security-audit.js';
6
+ export { createTokenValidator, createConfigTokenValidator, createHMACValidator, createManagedTokenValidator, combineValidators, } from './token-validator.js';
7
+ export { metricsCollector } from './metrics-collector.js';
8
+ export { TokenManager, initTokenManager, getTokenManager, type TokenInfo, type TokenManagerOptions, } from './token-manager.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 中间件统一导出
3
+ */
4
+ export { createRateLimit, defaultRateLimit } from './rate-limit.js';
5
+ export { initSecurityAudit, securityAudit, logAuthFailure, logInvalidToken, logSuspiciousRequest, logRateLimit, closeSecurityAudit, } from './security-audit.js';
6
+ export { createTokenValidator, createConfigTokenValidator, createHMACValidator, createManagedTokenValidator, combineValidators, } from './token-validator.js';
7
+ export { metricsCollector } from './metrics-collector.js';
8
+ export { TokenManager, initTokenManager, getTokenManager, } from './token-manager.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 性能指标收集中间件
3
+ * 自动收集请求数、响应时间、错误率等指标
4
+ */
5
+ import type { Context, Next } from 'koa';
6
+ /**
7
+ * 性能指标收集中间件
8
+ */
9
+ export declare function metricsCollector(): (ctx: Context, next: Next) => Promise<void>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 性能指标收集中间件
3
+ * 自动收集请求数、响应时间、错误率等指标
4
+ */
5
+ import { metrics } from '../metrics.js';
6
+ /**
7
+ * 性能指标收集中间件
8
+ */
9
+ export function metricsCollector() {
10
+ return async (ctx, next) => {
11
+ const startTime = Date.now();
12
+ const method = ctx.method;
13
+ const path = ctx.path;
14
+ // 增加请求计数
15
+ metrics.increment('http_requests_total', 1, {
16
+ method,
17
+ path: path.split('?')[0], // 移除查询参数
18
+ });
19
+ try {
20
+ await next();
21
+ const duration = Date.now() - startTime;
22
+ const status = ctx.status;
23
+ // 记录响应时间
24
+ metrics.observe('http_request_duration_ms', duration, {
25
+ method,
26
+ path: path.split('?')[0],
27
+ status: String(status),
28
+ });
29
+ // 记录状态码
30
+ metrics.increment('http_responses_total', 1, {
31
+ method,
32
+ path: path.split('?')[0],
33
+ status: String(status),
34
+ });
35
+ // 记录错误
36
+ if (status >= 400) {
37
+ metrics.increment('http_errors_total', 1, {
38
+ method,
39
+ path: path.split('?')[0],
40
+ status: String(status),
41
+ });
42
+ }
43
+ }
44
+ catch (error) {
45
+ const duration = Date.now() - startTime;
46
+ const status = ctx.status || 500;
47
+ // 记录错误响应时间
48
+ metrics.observe('http_request_duration_ms', duration, {
49
+ method,
50
+ path: path.split('?')[0],
51
+ status: String(status),
52
+ error: 'true',
53
+ });
54
+ // 记录错误
55
+ metrics.increment('http_errors_total', 1, {
56
+ method,
57
+ path: path.split('?')[0],
58
+ status: String(status),
59
+ error_type: error.name || 'Unknown',
60
+ });
61
+ throw error;
62
+ }
63
+ };
64
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 速率限制中间件
3
+ * 防止 API 滥用,保护服务器资源
4
+ */
5
+ import type { Context, Next } from 'koa';
6
+ interface RateLimitConfig {
7
+ /** 时间窗口(毫秒) */
8
+ windowMs: number;
9
+ /** 每个 IP 在时间窗口内的最大请求数 */
10
+ max: number;
11
+ /** 是否跳过成功请求(只限制失败请求) */
12
+ skipSuccessfulRequests?: boolean;
13
+ /** 是否跳过失败请求(只限制成功请求) */
14
+ skipFailedRequests?: boolean;
15
+ /** 自定义键生成函数 */
16
+ keyGenerator?: (ctx: Context) => string;
17
+ /** 跳过函数 */
18
+ skip?: (ctx: Context) => boolean;
19
+ /** 自定义错误消息 */
20
+ message?: string;
21
+ /** 自定义状态码 */
22
+ statusCode?: number;
23
+ }
24
+ /**
25
+ * 创建速率限制中间件
26
+ */
27
+ export declare function createRateLimit(config: RateLimitConfig): (ctx: Context, next: Next) => Promise<any>;
28
+ /**
29
+ * 默认速率限制配置
30
+ */
31
+ export declare const defaultRateLimit: (ctx: Context, next: Next) => Promise<any>;
32
+ export {};
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 速率限制中间件
3
+ * 防止 API 滥用,保护服务器资源
4
+ */
5
+ import { createLogger } from '../logger.js';
6
+ import { logRateLimit } from './security-audit.js';
7
+ /**
8
+ * 速率限制存储(内存实现)
9
+ * 生产环境建议使用 Redis
10
+ */
11
+ class MemoryStore {
12
+ store = {};
13
+ cleanupInterval;
14
+ constructor() {
15
+ // 每 5 分钟清理一次过期记录
16
+ this.cleanupInterval = setInterval(() => {
17
+ const now = Date.now();
18
+ for (const key in this.store) {
19
+ if (this.store[key].resetTime < now) {
20
+ delete this.store[key];
21
+ }
22
+ }
23
+ }, 5 * 60 * 1000);
24
+ }
25
+ /**
26
+ * 获取当前计数
27
+ */
28
+ get(key) {
29
+ const record = this.store[key];
30
+ if (!record) {
31
+ return null;
32
+ }
33
+ // 如果已过期,删除记录
34
+ if (record.resetTime < Date.now()) {
35
+ delete this.store[key];
36
+ return null;
37
+ }
38
+ return record;
39
+ }
40
+ /**
41
+ * 增加计数
42
+ */
43
+ increment(key, windowMs) {
44
+ const now = Date.now();
45
+ const record = this.store[key];
46
+ if (!record || record.resetTime < now) {
47
+ // 创建新记录
48
+ this.store[key] = {
49
+ count: 1,
50
+ resetTime: now + windowMs,
51
+ };
52
+ return this.store[key];
53
+ }
54
+ // 增加计数
55
+ record.count++;
56
+ return record;
57
+ }
58
+ /**
59
+ * 重置计数
60
+ */
61
+ reset(key) {
62
+ delete this.store[key];
63
+ }
64
+ /**
65
+ * 清理所有记录
66
+ */
67
+ cleanup() {
68
+ this.store = {};
69
+ }
70
+ /**
71
+ * 销毁存储
72
+ */
73
+ destroy() {
74
+ if (this.cleanupInterval) {
75
+ clearInterval(this.cleanupInterval);
76
+ }
77
+ this.cleanup();
78
+ }
79
+ }
80
+ /**
81
+ * 创建速率限制中间件
82
+ */
83
+ export function createRateLimit(config) {
84
+ const logger = createLogger('RateLimit');
85
+ const store = new MemoryStore();
86
+ const { windowMs = 60 * 1000, // 默认 1 分钟
87
+ max = 100, // 默认 100 次请求
88
+ skipSuccessfulRequests = false, skipFailedRequests = false, keyGenerator = (ctx) => {
89
+ // 默认使用 IP 地址作为键
90
+ return ctx.ip || ctx.request.ip || 'unknown';
91
+ }, skip = () => false, message = 'Too many requests, please try again later.', statusCode = 429, } = config;
92
+ return async (ctx, next) => {
93
+ // 检查是否跳过
94
+ if (skip(ctx)) {
95
+ return next();
96
+ }
97
+ // 生成键
98
+ const key = keyGenerator(ctx);
99
+ // 获取当前记录
100
+ const record = store.get(key);
101
+ if (record) {
102
+ // 检查是否超过限制
103
+ if (record.count >= max) {
104
+ logger.warn('Rate limit exceeded', {
105
+ key,
106
+ count: record.count,
107
+ max,
108
+ path: ctx.path,
109
+ method: ctx.method,
110
+ });
111
+ // 记录安全审计日志
112
+ logRateLimit(ctx, key, record.count, max);
113
+ ctx.status = statusCode;
114
+ ctx.body = {
115
+ error: 'RateLimitExceeded',
116
+ message,
117
+ retryAfter: Math.ceil((record.resetTime - Date.now()) / 1000),
118
+ };
119
+ // 设置响应头
120
+ ctx.set('X-RateLimit-Limit', String(max));
121
+ ctx.set('X-RateLimit-Remaining', '0');
122
+ ctx.set('X-RateLimit-Reset', String(Math.ceil(record.resetTime / 1000)));
123
+ ctx.set('Retry-After', String(Math.ceil((record.resetTime - Date.now()) / 1000)));
124
+ return;
125
+ }
126
+ }
127
+ // 执行下一个中间件
128
+ await next();
129
+ // 根据配置决定是否记录
130
+ const shouldSkip = (skipSuccessfulRequests && ctx.status < 400) ||
131
+ (skipFailedRequests && ctx.status >= 400);
132
+ if (!shouldSkip) {
133
+ // 增加计数
134
+ const newRecord = store.increment(key, windowMs);
135
+ // 设置响应头
136
+ ctx.set('X-RateLimit-Limit', String(max));
137
+ ctx.set('X-RateLimit-Remaining', String(Math.max(0, max - newRecord.count)));
138
+ ctx.set('X-RateLimit-Reset', String(Math.ceil(newRecord.resetTime / 1000)));
139
+ }
140
+ };
141
+ }
142
+ /**
143
+ * 默认速率限制配置
144
+ */
145
+ export const defaultRateLimit = createRateLimit({
146
+ windowMs: 60 * 1000, // 1 分钟
147
+ max: 100, // 100 次请求
148
+ message: 'Too many requests, please try again later.',
149
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 安全审计日志中间件
3
+ * 记录所有安全相关事件
4
+ */
5
+ import type { Context, Next } from 'koa';
6
+ /**
7
+ * 初始化安全审计日志
8
+ */
9
+ export declare function initSecurityAudit(auditLogDir: string): void;
10
+ /**
11
+ * 安全审计中间件
12
+ */
13
+ export declare function securityAudit(): (ctx: Context, next: Next) => Promise<any>;
14
+ /**
15
+ * 记录认证失败
16
+ */
17
+ export declare function logAuthFailure(ctx: Context, reason: string): void;
18
+ /**
19
+ * 记录无效令牌
20
+ */
21
+ export declare function logInvalidToken(ctx: Context, token?: string): void;
22
+ /**
23
+ * 记录可疑请求
24
+ */
25
+ export declare function logSuspiciousRequest(ctx: Context, reason: string, details?: Record<string, any>): void;
26
+ /**
27
+ * 记录速率限制触发
28
+ */
29
+ export declare function logRateLimit(ctx: Context, key: string, count: number, max: number): void;
30
+ /**
31
+ * 关闭审计日志
32
+ */
33
+ export declare function closeSecurityAudit(): void;