trustbase-seller-backend 0.7.8 → 0.8.0
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/core/chain-reconcile.js +4 -1
- package/middleware/api-key-auth.d.ts +65 -63
- package/middleware/api-key-auth.js +17 -3
- package/middleware/security.js +217 -216
- package/middleware/wallet-auth.js +162 -157
- package/oracle/c5-glue.js +787 -795
- package/package.json +1 -1
- package/routes/agent-pay.js +29 -20
- package/routes/agent-serviceprovider.js +174 -183
- package/routes/buyer-shop.js +5 -1
- package/routes/chain-writes-pledge.js +195 -204
- package/routes/chain-writes-products.js +430 -438
- package/routes/m2m-viewer.js +1 -1
- package/routes/onboard.js +961 -944
- package/routes/sp-applyment.js +291 -285
- package/routes/sp-notify.js +423 -424
- package/run-unified.js +9 -1
- package/test-agent-pay-lifecycle.js +217 -186
- package/test-agent-pay-wechat.js +323 -291
- package/test-api-key-auth-exempt.js +29 -1
- package/test-buyer-shop.js +359 -334
- package/test-chain-reconcile.js +2 -1
- package/test-onboard-boundary.js +51 -0
- package/test-pwa-id-encoding.js +42 -0
- package/test-relationship-chain.js +106 -102
- package/test-sp-applyment.js +588 -583
- package/test-sp-gateway.js +462 -435
- package/test-token-v2.js +171 -170
- package/web/dist/app.js +49 -13
- package/web/dist/index.html +3 -1
- package/web/dist/sw.js +12 -6
package/core/chain-reconcile.js
CHANGED
|
@@ -102,9 +102,12 @@ async function runChainReconcile({ db, lcdUrl }) {
|
|
|
102
102
|
summary.checked += 1;
|
|
103
103
|
const slot = activeBySku.get(p.id);
|
|
104
104
|
if (slot) {
|
|
105
|
+
// 本地质押是卖家原始快照,链上声誉调整后的 stake_per_unit 不参与一致性比较;
|
|
106
|
+
// 否则每次对账都会把同一商品重复记为 recovered(本地值又按设计保留)。
|
|
107
|
+
const hasLocalStake = p.chain_stake_per_unit !== null && p.chain_stake_per_unit !== undefined && p.chain_stake_per_unit !== "";
|
|
105
108
|
const needsRecover = !p.chain_listed || p.chain_sku_id !== slot.sku_id ||
|
|
106
109
|
Number(p.chain_slot_index) !== Number(slot.slot_index) ||
|
|
107
|
-
String(
|
|
110
|
+
(!hasLocalStake && String(slot.stake_per_unit || "") !== "");
|
|
108
111
|
if (needsRecover) {
|
|
109
112
|
updateLocalFromSlot(db, p, slot);
|
|
110
113
|
summary.recovered += 1;
|
|
@@ -1,64 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Key Authentication - API 密钥认证
|
|
3
|
-
*
|
|
4
|
-
* 类似 SSH 的双重验证:
|
|
5
|
-
* 1. API Key(类似 SSH 密钥)
|
|
6
|
-
* 2. 密码(类似 SSH 密码)
|
|
7
|
-
*
|
|
8
|
-
* 使用场景:
|
|
9
|
-
* - 外部访问必须验证
|
|
10
|
-
* - 局域网可跳过验证
|
|
11
|
-
*/
|
|
12
|
-
import { Request, Response, NextFunction } from 'express';
|
|
13
|
-
interface APIKey {
|
|
14
|
-
key: string;
|
|
15
|
-
keyPlaintext?: string;
|
|
16
|
-
name: string;
|
|
17
|
-
password: string;
|
|
18
|
-
createdAt: number;
|
|
19
|
-
lastUsedAt?: number;
|
|
20
|
-
permissions: string[];
|
|
21
|
-
ipWhitelist?: string[];
|
|
22
|
-
enabled: boolean;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* 生成 API Key
|
|
26
|
-
*/
|
|
27
|
-
export declare function generateAPIKey(): string;
|
|
28
|
-
/**
|
|
29
|
-
* 加载 API Keys
|
|
30
|
-
*/
|
|
31
|
-
export declare function loadAPIKeys(): APIKey[];
|
|
32
|
-
/**
|
|
33
|
-
* 保存 API Keys
|
|
34
|
-
*/
|
|
35
|
-
export declare function saveAPIKeys(keys: APIKey[]): void;
|
|
36
|
-
/**
|
|
37
|
-
* 创建新的 API Key(银行标准:加密存储)
|
|
38
|
-
*/
|
|
39
|
-
export declare function createAPIKey(name: string, password: string, permissions?: string[], ipWhitelist?: string[]): {
|
|
40
|
-
key: string;
|
|
41
|
-
password: string;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* 验证 API Key(银行标准:解密验证)
|
|
45
|
-
*/
|
|
46
|
-
export declare function validateAPIKey(key: string, password: string, clientIP?: string): {
|
|
47
|
-
valid: boolean;
|
|
48
|
-
apiKey?: APIKey;
|
|
49
|
-
error?: string;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* 撤销 API Key
|
|
53
|
-
*/
|
|
54
|
-
export declare function revokeAPIKey(key: string): boolean;
|
|
55
|
-
/**
|
|
56
|
-
* 列出所有 API Keys(隐藏密码)
|
|
57
|
-
*/
|
|
58
|
-
export declare function listAPIKeys(): Omit<APIKey, 'password'>[];
|
|
59
|
-
/**
|
|
60
|
-
* API Key 认证中间件(集成入侵检测)
|
|
61
|
-
*/
|
|
62
|
-
export declare function requireAPIKey(skipLAN?: boolean, exemptPaths?: string[]): (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
63
|
-
|
|
1
|
+
/**
|
|
2
|
+
* API Key Authentication - API 密钥认证
|
|
3
|
+
*
|
|
4
|
+
* 类似 SSH 的双重验证:
|
|
5
|
+
* 1. API Key(类似 SSH 密钥)
|
|
6
|
+
* 2. 密码(类似 SSH 密码)
|
|
7
|
+
*
|
|
8
|
+
* 使用场景:
|
|
9
|
+
* - 外部访问必须验证
|
|
10
|
+
* - 局域网可跳过验证
|
|
11
|
+
*/
|
|
12
|
+
import { Request, Response, NextFunction } from 'express';
|
|
13
|
+
interface APIKey {
|
|
14
|
+
key: string;
|
|
15
|
+
keyPlaintext?: string;
|
|
16
|
+
name: string;
|
|
17
|
+
password: string;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
lastUsedAt?: number;
|
|
20
|
+
permissions: string[];
|
|
21
|
+
ipWhitelist?: string[];
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 生成 API Key
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateAPIKey(): string;
|
|
28
|
+
/**
|
|
29
|
+
* 加载 API Keys
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadAPIKeys(): APIKey[];
|
|
32
|
+
/**
|
|
33
|
+
* 保存 API Keys
|
|
34
|
+
*/
|
|
35
|
+
export declare function saveAPIKeys(keys: APIKey[]): void;
|
|
36
|
+
/**
|
|
37
|
+
* 创建新的 API Key(银行标准:加密存储)
|
|
38
|
+
*/
|
|
39
|
+
export declare function createAPIKey(name: string, password: string, permissions?: string[], ipWhitelist?: string[]): {
|
|
40
|
+
key: string;
|
|
41
|
+
password: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 验证 API Key(银行标准:解密验证)
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateAPIKey(key: string, password: string, clientIP?: string): {
|
|
47
|
+
valid: boolean;
|
|
48
|
+
apiKey?: APIKey;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* 撤销 API Key
|
|
53
|
+
*/
|
|
54
|
+
export declare function revokeAPIKey(key: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 列出所有 API Keys(隐藏密码)
|
|
57
|
+
*/
|
|
58
|
+
export declare function listAPIKeys(): Omit<APIKey, 'password'>[];
|
|
59
|
+
/**
|
|
60
|
+
* API Key 认证中间件(集成入侵检测)
|
|
61
|
+
*/
|
|
62
|
+
export declare function requireAPIKey(skipLAN?: boolean, exemptPaths?: string[]): (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
63
|
+
/** 仅在回环隧道请求中采信网关注入的 XFF,其他请求使用 socket 对端 IP。 */
|
|
64
|
+
export declare function getRateLimitClientIP(req: Request, socketIP: string): string;
|
|
65
|
+
export {};
|
|
64
66
|
//# sourceMappingURL=api-key-auth.d.ts.map
|
|
@@ -22,6 +22,7 @@ exports.validateAPIKey = validateAPIKey;
|
|
|
22
22
|
exports.revokeAPIKey = revokeAPIKey;
|
|
23
23
|
exports.listAPIKeys = listAPIKeys;
|
|
24
24
|
exports.requireAPIKey = requireAPIKey;
|
|
25
|
+
exports.getRateLimitClientIP = getRateLimitClientIP;
|
|
25
26
|
const crypto_1 = __importDefault(require("crypto"));
|
|
26
27
|
const fs_1 = __importDefault(require("fs"));
|
|
27
28
|
const path_1 = __importDefault(require("path"));
|
|
@@ -175,9 +176,22 @@ function isTunnelBuyerRequest(req, clientIP) {
|
|
|
175
176
|
return TUNNEL_BUYER_PATHS.some(p => paths.some(x => x === p || x.startsWith(p + '/')));
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
function getRateLimitClientIP(req, socketIP) {
|
|
180
|
+
const isLoopback = socketIP === '127.0.0.1' || socketIP === '::1' || /^127\./.test(socketIP);
|
|
181
|
+
// 2026-10-18 fix: 公网经隧道时, socket 对端恒为 127.0.0.1,
|
|
182
|
+
// 若直接用 socketIP 做限流桶 key, 全体公网买家会共享一个桶。
|
|
183
|
+
// 只在"回环 + 隧道头"时采信 XFF 第一个非空 IP, 否则回退 socketIP。
|
|
184
|
+
if (isLoopback && String(req.headers['x-tb-tunnel'] || '') === '1') {
|
|
185
|
+
const first = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
|
|
186
|
+
if (first) return first;
|
|
187
|
+
}
|
|
188
|
+
return socketIP;
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function requireAPIKey(skipLAN = true, exemptPaths = []) { return (req, res, next) => {
|
|
179
|
-
//
|
|
180
|
-
const
|
|
192
|
+
// 获取 socket 对端 IP (用于隧道回环判定) 与限流桶 IP (用于限流)
|
|
193
|
+
const socketIP = (req.ip || req.connection.remoteAddress || '').replace('::ffff:', '');
|
|
194
|
+
const clientIP = getRateLimitClientIP(req, socketIP);
|
|
181
195
|
// 1. 检查 IP 黑名单
|
|
182
196
|
if ((0, intrusion_detection_1.isIPBlacklisted)(clientIP)) {
|
|
183
197
|
return res.status(403).json({
|
|
@@ -211,7 +225,7 @@ function requireAPIKey(skipLAN = true, exemptPaths = []) { return (req, res,
|
|
|
211
225
|
// 而商户机默认 REQUIRE_API_KEY_FOR_LAN=1 → 回环也不放行 → 买家拿到 401, 公网店店等于不可用。
|
|
212
226
|
// 口径收窄到三者同时成立: ① 来源是回环(只有本机进程能发这个头) ② 带 X-TB-Tunnel 头(隧道客户端转发时加)
|
|
213
227
|
// ③ 命中买家面路径(商品/订单读)。管理面 API 一律仍要密钥。
|
|
214
|
-
if (isTunnelBuyerRequest(req,
|
|
228
|
+
if (isTunnelBuyerRequest(req, socketIP)) {
|
|
215
229
|
return next();
|
|
216
230
|
}
|
|
217
231
|
// 跳过局域网(开发模式)
|
package/middleware/security.js
CHANGED
|
@@ -1,217 +1,218 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Security Middleware - 安全中间件
|
|
4
|
-
*
|
|
5
|
-
* 功能:
|
|
6
|
-
* 1. IP 白名单验证
|
|
7
|
-
* 2. CORS 限制
|
|
8
|
-
* 3. 请求日志审计
|
|
9
|
-
* 4. 安全头部
|
|
10
|
-
*/
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.requestSizeLimit = void 0;
|
|
16
|
-
exports.ipWhitelist = ipWhitelist;
|
|
17
|
-
exports.createCorsOptions = createCorsOptions;
|
|
18
|
-
exports.auditLog = auditLog;
|
|
19
|
-
exports.securityHeaders = securityHeaders;
|
|
20
|
-
exports.validateEnv = validateEnv;
|
|
21
|
-
const fs_1 = __importDefault(require("fs"));
|
|
22
|
-
const path_1 = __importDefault(require("path"));
|
|
23
|
-
// ===================== IP 白名单 =====================
|
|
24
|
-
/**
|
|
25
|
-
* 检查 IP 是否在 CIDR 范围内
|
|
26
|
-
*/
|
|
27
|
-
function isIPInCIDR(ip, cidr) {
|
|
28
|
-
// 简化实现,生产环境建议使用 ip-range-check 库
|
|
29
|
-
if (cidr.endsWith('/24')) {
|
|
30
|
-
const network = cidr.replace('/24', '');
|
|
31
|
-
const ipParts = ip.split('.');
|
|
32
|
-
const networkParts = network.split('.');
|
|
33
|
-
return ipParts[0] === networkParts[0] &&
|
|
34
|
-
ipParts[1] === networkParts[1] &&
|
|
35
|
-
ipParts[2] === networkParts[2];
|
|
36
|
-
}
|
|
37
|
-
return ip === cidr;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* IP 白名单中间件
|
|
41
|
-
*/
|
|
42
|
-
function ipWhitelist(allowedIPs) {
|
|
43
|
-
return (req, res, next) => {
|
|
44
|
-
// 如果白名单为空,允许所有访问
|
|
45
|
-
if (!allowedIPs || allowedIPs.length === 0) {
|
|
46
|
-
return next();
|
|
47
|
-
}
|
|
48
|
-
const clientIP = (req.ip ||
|
|
49
|
-
req.connection.remoteAddress ||
|
|
50
|
-
req.headers['x-forwarded-for'] ||
|
|
51
|
-
'unknown')
|
|
52
|
-
.replace('::ffff:', ''); // IPv4 映射地址
|
|
53
|
-
const isAllowed = allowedIPs.some(ip => {
|
|
54
|
-
if (ip.includes('/')) {
|
|
55
|
-
return isIPInCIDR(clientIP, ip);
|
|
56
|
-
}
|
|
57
|
-
return clientIP === ip || clientIP.includes(ip);
|
|
58
|
-
});
|
|
59
|
-
if (!isAllowed) {
|
|
60
|
-
console.warn(`[Security] 拒绝访问: ${clientIP}`);
|
|
61
|
-
return res.status(403).json({
|
|
62
|
-
error: '禁止访问',
|
|
63
|
-
message: '您的 IP 不在白名单中'
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
next();
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// ===================== CORS 配置 =====================
|
|
70
|
-
/**
|
|
71
|
-
* 动态 CORS 配置
|
|
72
|
-
*/
|
|
73
|
-
function createCorsOptions(allowedOrigins) {
|
|
74
|
-
return {
|
|
75
|
-
origin: (origin, callback) => {
|
|
76
|
-
// 允许没有 origin 的请求(如移动 APP、Postman)
|
|
77
|
-
if (!origin)
|
|
78
|
-
return callback(null, true);
|
|
79
|
-
// 2026-08-13 kimi 打磨 (xiaomu 浏览器 500 根因): 同源/本机/局域网来源一律放行.
|
|
80
|
-
// 模块脚本会带 Origin — 自己服务的 UI 打自己的 API 必须是同源自通, 与 LAN 免密同一信任模型.
|
|
81
|
-
try {
|
|
82
|
-
const host = new URL(origin).hostname;
|
|
83
|
-
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]' ||
|
|
84
|
-
host.startsWith('192.168.') || host.startsWith('10.') || /^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
85
|
-
host.startsWith('fe80:') || host.startsWith('240') // 链路本地 + 国内公网 v6 段 (240e/2408/2409 等)
|
|
86
|
-
) {
|
|
87
|
-
return callback(null, true);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch { /* 畸形 origin 继续走列表判断 */ }
|
|
91
|
-
// 空配置 fallback (baobei 15442): 没配 allowedOrigins 时放行, 不默默拒绝所有 (开发模式).
|
|
92
|
-
// 生产部署必须显式配 ALLOWED_ORIGINS (或依赖上面的 LAN 放行).
|
|
93
|
-
if (allowedOrigins.length === 0) {
|
|
94
|
-
console.warn('[CORS] ALLOWED_ORIGINS 为空, 放行所有来源 (开发模式, 生产请显式配置)');
|
|
95
|
-
return callback(null, true);
|
|
96
|
-
}
|
|
97
|
-
// 检查是否在允许列表
|
|
98
|
-
const isAllowed = allowedOrigins.some(allowedOrigin => {
|
|
99
|
-
if (allowedOrigin.startsWith('regex:')) {
|
|
100
|
-
// 正则表达式匹配
|
|
101
|
-
const pattern = allowedOrigin.replace('regex:', '');
|
|
102
|
-
return new RegExp(pattern).test(origin);
|
|
103
|
-
}
|
|
104
|
-
return allowedOrigin === origin;
|
|
105
|
-
});
|
|
106
|
-
if (isAllowed) {
|
|
107
|
-
callback(null, true);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
console.warn(`[CORS] 拒绝来源: ${origin}`);
|
|
111
|
-
callback(new Error('不允许的来源'));
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
credentials: true,
|
|
115
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
116
|
-
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
// ===================== 请求审计日志 =====================
|
|
120
|
-
const auditLogFile = path_1.default.join(__dirname, '../../logs/audit.log');
|
|
121
|
-
/**
|
|
122
|
-
* 确保日志目录存在
|
|
123
|
-
*/
|
|
124
|
-
function ensureLogDir() {
|
|
125
|
-
const logDir = path_1.default.dirname(auditLogFile);
|
|
126
|
-
if (!fs_1.default.existsSync(logDir)) {
|
|
127
|
-
fs_1.default.mkdirSync(logDir, { recursive: true });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* 审计日志中间件
|
|
132
|
-
*/
|
|
133
|
-
function auditLog() {
|
|
134
|
-
ensureLogDir();
|
|
135
|
-
return (req, res, next) => {
|
|
136
|
-
const startTime = Date.now();
|
|
137
|
-
// 记录响应完成
|
|
138
|
-
res.on('finish', () => {
|
|
139
|
-
const log = {
|
|
140
|
-
timestamp: new Date().toISOString(),
|
|
141
|
-
method: req.method,
|
|
142
|
-
path: req.path,
|
|
143
|
-
query: req.query,
|
|
144
|
-
ip: req.ip || req.connection.remoteAddress,
|
|
145
|
-
userAgent: req.get('user-agent'),
|
|
146
|
-
statusCode: res.statusCode,
|
|
147
|
-
responseTime: Date.now() - startTime,
|
|
148
|
-
userId: req.user?.walletAddress || 'anonymous',
|
|
149
|
-
};
|
|
150
|
-
// 写入日志文件
|
|
151
|
-
const logLine = JSON.stringify(log) + '\n';
|
|
152
|
-
fs_1.default.appendFile(auditLogFile, logLine, (err) => {
|
|
153
|
-
if (err)
|
|
154
|
-
console.error('[Audit] 写入日志失败:', err);
|
|
155
|
-
});
|
|
156
|
-
// 控制台输出(可选)
|
|
157
|
-
if (process.env.LOG_LEVEL === 'debug') {
|
|
158
|
-
console.log('[Audit]', log.method, log.path, log.statusCode, `${log.responseTime}ms`);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
next();
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
// ===================== 安全头部 =====================
|
|
165
|
-
/**
|
|
166
|
-
* 安全头部中间件
|
|
167
|
-
*/
|
|
168
|
-
function securityHeaders() {
|
|
169
|
-
return (req, res, next) => {
|
|
170
|
-
// 防止 XSS 攻击
|
|
171
|
-
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
172
|
-
// 防止 MIME 类型嗅探
|
|
173
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
174
|
-
// 防止点击劫持
|
|
175
|
-
res.setHeader('X-Frame-Options', 'DENY');
|
|
176
|
-
// 内容安全策略 (baobei 15442: 扩 script-src/style-src 让 vue-i18n runtime 模板编译能跑)
|
|
177
|
-
// 治标: Vite production bundle 用 runtime 编译 (new Function), 需要 unsafe-eval; 真修是前端 runtime-only (见 baobei 清单)
|
|
178
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; " +
|
|
179
|
-
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
|
180
|
-
"style-src 'self' 'unsafe-inline'; " +
|
|
181
|
-
"img-src 'self' data: https:; " +
|
|
182
|
-
"font-src 'self' data:; " +
|
|
183
|
-
"connect-src 'self' ws: wss: https:; " +
|
|
184
|
-
"object-src 'none'; base-uri 'self'; frame-ancestors 'none'");
|
|
185
|
-
// 隐藏服务器信息
|
|
186
|
-
res.removeHeader('X-Powered-By');
|
|
187
|
-
next();
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
// ===================== 请求大小限制 =====================
|
|
191
|
-
/**
|
|
192
|
-
* 请求体大小限制
|
|
193
|
-
*/
|
|
194
|
-
exports.requestSizeLimit = {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
console.error('
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
console.warn('⚠️
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security Middleware - 安全中间件
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* 1. IP 白名单验证
|
|
7
|
+
* 2. CORS 限制
|
|
8
|
+
* 3. 请求日志审计
|
|
9
|
+
* 4. 安全头部
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.requestSizeLimit = void 0;
|
|
16
|
+
exports.ipWhitelist = ipWhitelist;
|
|
17
|
+
exports.createCorsOptions = createCorsOptions;
|
|
18
|
+
exports.auditLog = auditLog;
|
|
19
|
+
exports.securityHeaders = securityHeaders;
|
|
20
|
+
exports.validateEnv = validateEnv;
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
// ===================== IP 白名单 =====================
|
|
24
|
+
/**
|
|
25
|
+
* 检查 IP 是否在 CIDR 范围内
|
|
26
|
+
*/
|
|
27
|
+
function isIPInCIDR(ip, cidr) {
|
|
28
|
+
// 简化实现,生产环境建议使用 ip-range-check 库
|
|
29
|
+
if (cidr.endsWith('/24')) {
|
|
30
|
+
const network = cidr.replace('/24', '');
|
|
31
|
+
const ipParts = ip.split('.');
|
|
32
|
+
const networkParts = network.split('.');
|
|
33
|
+
return ipParts[0] === networkParts[0] &&
|
|
34
|
+
ipParts[1] === networkParts[1] &&
|
|
35
|
+
ipParts[2] === networkParts[2];
|
|
36
|
+
}
|
|
37
|
+
return ip === cidr;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* IP 白名单中间件
|
|
41
|
+
*/
|
|
42
|
+
function ipWhitelist(allowedIPs) {
|
|
43
|
+
return (req, res, next) => {
|
|
44
|
+
// 如果白名单为空,允许所有访问
|
|
45
|
+
if (!allowedIPs || allowedIPs.length === 0) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
const clientIP = (req.ip ||
|
|
49
|
+
req.connection.remoteAddress ||
|
|
50
|
+
req.headers['x-forwarded-for'] ||
|
|
51
|
+
'unknown')
|
|
52
|
+
.replace('::ffff:', ''); // IPv4 映射地址
|
|
53
|
+
const isAllowed = allowedIPs.some(ip => {
|
|
54
|
+
if (ip.includes('/')) {
|
|
55
|
+
return isIPInCIDR(clientIP, ip);
|
|
56
|
+
}
|
|
57
|
+
return clientIP === ip || clientIP.includes(ip);
|
|
58
|
+
});
|
|
59
|
+
if (!isAllowed) {
|
|
60
|
+
console.warn(`[Security] 拒绝访问: ${clientIP}`);
|
|
61
|
+
return res.status(403).json({
|
|
62
|
+
error: '禁止访问',
|
|
63
|
+
message: '您的 IP 不在白名单中'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ===================== CORS 配置 =====================
|
|
70
|
+
/**
|
|
71
|
+
* 动态 CORS 配置
|
|
72
|
+
*/
|
|
73
|
+
function createCorsOptions(allowedOrigins) {
|
|
74
|
+
return {
|
|
75
|
+
origin: (origin, callback) => {
|
|
76
|
+
// 允许没有 origin 的请求(如移动 APP、Postman)
|
|
77
|
+
if (!origin)
|
|
78
|
+
return callback(null, true);
|
|
79
|
+
// 2026-08-13 kimi 打磨 (xiaomu 浏览器 500 根因): 同源/本机/局域网来源一律放行.
|
|
80
|
+
// 模块脚本会带 Origin — 自己服务的 UI 打自己的 API 必须是同源自通, 与 LAN 免密同一信任模型.
|
|
81
|
+
try {
|
|
82
|
+
const host = new URL(origin).hostname;
|
|
83
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]' ||
|
|
84
|
+
host.startsWith('192.168.') || host.startsWith('10.') || /^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
85
|
+
host.startsWith('fe80:') || host.startsWith('240') // 链路本地 + 国内公网 v6 段 (240e/2408/2409 等)
|
|
86
|
+
) {
|
|
87
|
+
return callback(null, true);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* 畸形 origin 继续走列表判断 */ }
|
|
91
|
+
// 空配置 fallback (baobei 15442): 没配 allowedOrigins 时放行, 不默默拒绝所有 (开发模式).
|
|
92
|
+
// 生产部署必须显式配 ALLOWED_ORIGINS (或依赖上面的 LAN 放行).
|
|
93
|
+
if (allowedOrigins.length === 0) {
|
|
94
|
+
console.warn('[CORS] ALLOWED_ORIGINS 为空, 放行所有来源 (开发模式, 生产请显式配置)');
|
|
95
|
+
return callback(null, true);
|
|
96
|
+
}
|
|
97
|
+
// 检查是否在允许列表
|
|
98
|
+
const isAllowed = allowedOrigins.some(allowedOrigin => {
|
|
99
|
+
if (allowedOrigin.startsWith('regex:')) {
|
|
100
|
+
// 正则表达式匹配
|
|
101
|
+
const pattern = allowedOrigin.replace('regex:', '');
|
|
102
|
+
return new RegExp(pattern).test(origin);
|
|
103
|
+
}
|
|
104
|
+
return allowedOrigin === origin;
|
|
105
|
+
});
|
|
106
|
+
if (isAllowed) {
|
|
107
|
+
callback(null, true);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.warn(`[CORS] 拒绝来源: ${origin}`);
|
|
111
|
+
callback(new Error('不允许的来源'));
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
credentials: true,
|
|
115
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
116
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ===================== 请求审计日志 =====================
|
|
120
|
+
const auditLogFile = path_1.default.join(__dirname, '../../logs/audit.log');
|
|
121
|
+
/**
|
|
122
|
+
* 确保日志目录存在
|
|
123
|
+
*/
|
|
124
|
+
function ensureLogDir() {
|
|
125
|
+
const logDir = path_1.default.dirname(auditLogFile);
|
|
126
|
+
if (!fs_1.default.existsSync(logDir)) {
|
|
127
|
+
fs_1.default.mkdirSync(logDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 审计日志中间件
|
|
132
|
+
*/
|
|
133
|
+
function auditLog() {
|
|
134
|
+
ensureLogDir();
|
|
135
|
+
return (req, res, next) => {
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
// 记录响应完成
|
|
138
|
+
res.on('finish', () => {
|
|
139
|
+
const log = {
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
method: req.method,
|
|
142
|
+
path: req.path,
|
|
143
|
+
query: req.query,
|
|
144
|
+
ip: req.ip || req.connection.remoteAddress,
|
|
145
|
+
userAgent: req.get('user-agent'),
|
|
146
|
+
statusCode: res.statusCode,
|
|
147
|
+
responseTime: Date.now() - startTime,
|
|
148
|
+
userId: req.user?.walletAddress || 'anonymous',
|
|
149
|
+
};
|
|
150
|
+
// 写入日志文件
|
|
151
|
+
const logLine = JSON.stringify(log) + '\n';
|
|
152
|
+
fs_1.default.appendFile(auditLogFile, logLine, (err) => {
|
|
153
|
+
if (err)
|
|
154
|
+
console.error('[Audit] 写入日志失败:', err);
|
|
155
|
+
});
|
|
156
|
+
// 控制台输出(可选)
|
|
157
|
+
if (process.env.LOG_LEVEL === 'debug') {
|
|
158
|
+
console.log('[Audit]', log.method, log.path, log.statusCode, `${log.responseTime}ms`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
next();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ===================== 安全头部 =====================
|
|
165
|
+
/**
|
|
166
|
+
* 安全头部中间件
|
|
167
|
+
*/
|
|
168
|
+
function securityHeaders() {
|
|
169
|
+
return (req, res, next) => {
|
|
170
|
+
// 防止 XSS 攻击
|
|
171
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
172
|
+
// 防止 MIME 类型嗅探
|
|
173
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
174
|
+
// 防止点击劫持
|
|
175
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
176
|
+
// 内容安全策略 (baobei 15442: 扩 script-src/style-src 让 vue-i18n runtime 模板编译能跑)
|
|
177
|
+
// 治标: Vite production bundle 用 runtime 编译 (new Function), 需要 unsafe-eval; 真修是前端 runtime-only (见 baobei 清单)
|
|
178
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; " +
|
|
179
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
|
180
|
+
"style-src 'self' 'unsafe-inline'; " +
|
|
181
|
+
"img-src 'self' data: https:; " +
|
|
182
|
+
"font-src 'self' data:; " +
|
|
183
|
+
"connect-src 'self' ws: wss: https:; " +
|
|
184
|
+
"object-src 'none'; base-uri 'self'; frame-ancestors 'none'");
|
|
185
|
+
// 隐藏服务器信息
|
|
186
|
+
res.removeHeader('X-Powered-By');
|
|
187
|
+
next();
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// ===================== 请求大小限制 =====================
|
|
191
|
+
/**
|
|
192
|
+
* 请求体大小限制
|
|
193
|
+
*/
|
|
194
|
+
exports.requestSizeLimit = {
|
|
195
|
+
// 950KB 图片压缩阈值还要叠加 base64/JSON 字段开销,1MB 会在边界误伤正常进件。
|
|
196
|
+
json: { limit: '2mb' }, // JSON 请求体最大 2MB
|
|
197
|
+
urlencoded: { limit: '1mb', extended: true }, // URL 编码请求体最大 1MB
|
|
198
|
+
};
|
|
199
|
+
// ===================== 环境变量验证 =====================
|
|
200
|
+
/**
|
|
201
|
+
* 验证必需的环境变量
|
|
202
|
+
*/
|
|
203
|
+
function validateEnv() {
|
|
204
|
+
const required = ['JWT_SECRET'];
|
|
205
|
+
const missing = required.filter(key => !process.env[key]);
|
|
206
|
+
if (missing.length > 0) {
|
|
207
|
+
console.error('❌ 缺少必需的环境变量:', missing.join(', '));
|
|
208
|
+
console.error('请创建 .env 文件并设置这些变量');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
// 检查是否使用默认 JWT 密钥
|
|
212
|
+
if (process.env.JWT_SECRET === 'trustchain-secret-key-2026') {
|
|
213
|
+
console.warn('⚠️ 警告: 使用默认 JWT 密钥不安全!');
|
|
214
|
+
console.warn('⚠️ 请在 .env 文件中设置 JWT_SECRET');
|
|
215
|
+
}
|
|
216
|
+
console.log('✅ 环境变量验证通过');
|
|
217
|
+
}
|
|
217
218
|
//# sourceMappingURL=security.js.map
|