@usethink/cf-core 0.3.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.
Files changed (169) hide show
  1. package/README.md +88 -0
  2. package/dist/features/anti-abuse/index.d.ts +62 -0
  3. package/dist/features/anti-abuse/index.d.ts.map +1 -0
  4. package/dist/features/anti-abuse/index.js +139 -0
  5. package/dist/features/anti-abuse/index.js.map +1 -0
  6. package/dist/features/email/index.d.ts +68 -0
  7. package/dist/features/email/index.d.ts.map +1 -0
  8. package/dist/features/email/index.js +120 -0
  9. package/dist/features/email/index.js.map +1 -0
  10. package/dist/features/payment/fetch-utils.d.ts +17 -0
  11. package/dist/features/payment/fetch-utils.d.ts.map +1 -0
  12. package/dist/features/payment/fetch-utils.js +56 -0
  13. package/dist/features/payment/fetch-utils.js.map +1 -0
  14. package/dist/features/payment/index.d.ts +20 -0
  15. package/dist/features/payment/index.d.ts.map +1 -0
  16. package/dist/features/payment/index.js +21 -0
  17. package/dist/features/payment/index.js.map +1 -0
  18. package/dist/features/payment/providers/alipay.d.ts +30 -0
  19. package/dist/features/payment/providers/alipay.d.ts.map +1 -0
  20. package/dist/features/payment/providers/alipay.js +149 -0
  21. package/dist/features/payment/providers/alipay.js.map +1 -0
  22. package/dist/features/payment/providers/stripe.d.ts +34 -0
  23. package/dist/features/payment/providers/stripe.d.ts.map +1 -0
  24. package/dist/features/payment/providers/stripe.js +168 -0
  25. package/dist/features/payment/providers/stripe.js.map +1 -0
  26. package/dist/features/payment/providers/trc20.d.ts +24 -0
  27. package/dist/features/payment/providers/trc20.d.ts.map +1 -0
  28. package/dist/features/payment/providers/trc20.js +96 -0
  29. package/dist/features/payment/providers/trc20.js.map +1 -0
  30. package/dist/features/payment/registry.d.ts +43 -0
  31. package/dist/features/payment/registry.d.ts.map +1 -0
  32. package/dist/features/payment/registry.js +65 -0
  33. package/dist/features/payment/registry.js.map +1 -0
  34. package/dist/features/payment/types.d.ts +72 -0
  35. package/dist/features/payment/types.d.ts.map +1 -0
  36. package/dist/features/payment/types.js +8 -0
  37. package/dist/features/payment/types.js.map +1 -0
  38. package/dist/features/thompson-router/index.d.ts +101 -0
  39. package/dist/features/thompson-router/index.d.ts.map +1 -0
  40. package/dist/features/thompson-router/index.js +186 -0
  41. package/dist/features/thompson-router/index.js.map +1 -0
  42. package/dist/features/webhook/index.d.ts +76 -0
  43. package/dist/features/webhook/index.d.ts.map +1 -0
  44. package/dist/features/webhook/index.js +127 -0
  45. package/dist/features/webhook/index.js.map +1 -0
  46. package/dist/src/audit.d.ts +45 -0
  47. package/dist/src/audit.d.ts.map +1 -0
  48. package/dist/src/audit.js +40 -0
  49. package/dist/src/audit.js.map +1 -0
  50. package/dist/src/auth/jwt.d.ts +33 -0
  51. package/dist/src/auth/jwt.d.ts.map +1 -0
  52. package/dist/src/auth/jwt.js +87 -0
  53. package/dist/src/auth/jwt.js.map +1 -0
  54. package/dist/src/auth/password.d.ts +26 -0
  55. package/dist/src/auth/password.d.ts.map +1 -0
  56. package/dist/src/auth/password.js +52 -0
  57. package/dist/src/auth/password.js.map +1 -0
  58. package/dist/src/bootstrap.d.ts +74 -0
  59. package/dist/src/bootstrap.d.ts.map +1 -0
  60. package/dist/src/bootstrap.js +231 -0
  61. package/dist/src/bootstrap.js.map +1 -0
  62. package/dist/src/cache.d.ts +52 -0
  63. package/dist/src/cache.d.ts.map +1 -0
  64. package/dist/src/cache.js +76 -0
  65. package/dist/src/cache.js.map +1 -0
  66. package/dist/src/config.d.ts +83 -0
  67. package/dist/src/config.d.ts.map +1 -0
  68. package/dist/src/config.js +96 -0
  69. package/dist/src/config.js.map +1 -0
  70. package/dist/src/crypto.d.ts +33 -0
  71. package/dist/src/crypto.d.ts.map +1 -0
  72. package/dist/src/crypto.js +87 -0
  73. package/dist/src/crypto.js.map +1 -0
  74. package/dist/src/db/connection.d.ts +53 -0
  75. package/dist/src/db/connection.d.ts.map +1 -0
  76. package/dist/src/db/connection.js +104 -0
  77. package/dist/src/db/connection.js.map +1 -0
  78. package/dist/src/db/index.d.ts +6 -0
  79. package/dist/src/db/index.d.ts.map +1 -0
  80. package/dist/src/db/index.js +6 -0
  81. package/dist/src/db/index.js.map +1 -0
  82. package/dist/src/db/schema.d.ts +649 -0
  83. package/dist/src/db/schema.d.ts.map +1 -0
  84. package/dist/src/db/schema.js +76 -0
  85. package/dist/src/db/schema.js.map +1 -0
  86. package/dist/src/error.d.ts +47 -0
  87. package/dist/src/error.d.ts.map +1 -0
  88. package/dist/src/error.js +94 -0
  89. package/dist/src/error.js.map +1 -0
  90. package/dist/src/http.d.ts +83 -0
  91. package/dist/src/http.d.ts.map +1 -0
  92. package/dist/src/http.js +116 -0
  93. package/dist/src/http.js.map +1 -0
  94. package/dist/src/idempotency.d.ts +78 -0
  95. package/dist/src/idempotency.d.ts.map +1 -0
  96. package/dist/src/idempotency.js +84 -0
  97. package/dist/src/idempotency.js.map +1 -0
  98. package/dist/src/index.d.ts +31 -0
  99. package/dist/src/index.d.ts.map +1 -0
  100. package/dist/src/index.js +45 -0
  101. package/dist/src/index.js.map +1 -0
  102. package/dist/src/logger.d.ts +31 -0
  103. package/dist/src/logger.d.ts.map +1 -0
  104. package/dist/src/logger.js +45 -0
  105. package/dist/src/logger.js.map +1 -0
  106. package/dist/src/middleware/admin-auth.d.ts +38 -0
  107. package/dist/src/middleware/admin-auth.d.ts.map +1 -0
  108. package/dist/src/middleware/admin-auth.js +55 -0
  109. package/dist/src/middleware/admin-auth.js.map +1 -0
  110. package/dist/src/middleware/api-key-auth.d.ts +42 -0
  111. package/dist/src/middleware/api-key-auth.d.ts.map +1 -0
  112. package/dist/src/middleware/api-key-auth.js +104 -0
  113. package/dist/src/middleware/api-key-auth.js.map +1 -0
  114. package/dist/src/middleware/index.d.ts +3 -0
  115. package/dist/src/middleware/index.d.ts.map +1 -0
  116. package/dist/src/middleware/index.js +3 -0
  117. package/dist/src/middleware/index.js.map +1 -0
  118. package/dist/src/rate-limit.d.ts +54 -0
  119. package/dist/src/rate-limit.d.ts.map +1 -0
  120. package/dist/src/rate-limit.js +134 -0
  121. package/dist/src/rate-limit.js.map +1 -0
  122. package/dist/src/security.d.ts +78 -0
  123. package/dist/src/security.d.ts.map +1 -0
  124. package/dist/src/security.js +175 -0
  125. package/dist/src/security.js.map +1 -0
  126. package/dist/src/types.d.ts +64 -0
  127. package/dist/src/types.d.ts.map +1 -0
  128. package/dist/src/types.js +8 -0
  129. package/dist/src/types.js.map +1 -0
  130. package/features/anti-abuse/index.ts +180 -0
  131. package/features/anti-abuse/tests/index.test.ts +50 -0
  132. package/features/email/index.ts +172 -0
  133. package/features/email/tests/index.test.ts +44 -0
  134. package/features/payment/fetch-utils.ts +65 -0
  135. package/features/payment/index.ts +39 -0
  136. package/features/payment/providers/alipay.ts +171 -0
  137. package/features/payment/providers/stripe.ts +192 -0
  138. package/features/payment/providers/trc20.ts +115 -0
  139. package/features/payment/registry.ts +87 -0
  140. package/features/payment/tests/index.test.ts +506 -0
  141. package/features/payment/types.ts +93 -0
  142. package/features/telegram-miniapp/index.ts +109 -0
  143. package/features/telegram-miniapp/tests/index.test.ts +11 -0
  144. package/features/thompson-router/index.ts +243 -0
  145. package/features/thompson-router/tests/index.test.ts +93 -0
  146. package/features/webhook/index.ts +183 -0
  147. package/features/webhook/tests/index.test.ts +21 -0
  148. package/package.json +202 -0
  149. package/src/audit.ts +70 -0
  150. package/src/auth/jwt.ts +114 -0
  151. package/src/auth/password.ts +75 -0
  152. package/src/bootstrap.ts +322 -0
  153. package/src/cache.ts +78 -0
  154. package/src/config.ts +134 -0
  155. package/src/crypto.ts +106 -0
  156. package/src/db/connection.ts +127 -0
  157. package/src/db/index.ts +6 -0
  158. package/src/db/schema.ts +90 -0
  159. package/src/error.ts +125 -0
  160. package/src/http.ts +150 -0
  161. package/src/idempotency.ts +127 -0
  162. package/src/index.ts +85 -0
  163. package/src/logger.ts +63 -0
  164. package/src/middleware/admin-auth.ts +71 -0
  165. package/src/middleware/api-key-auth.ts +164 -0
  166. package/src/middleware/index.ts +2 -0
  167. package/src/rate-limit.ts +167 -0
  168. package/src/security.ts +219 -0
  169. package/src/types.ts +70 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 安全工具模块
3
+ *
4
+ * 提供 SHA-256 哈希、IP 哈希(加盐)、时序安全比较、Turnstile 人机验证、安全响应头。
5
+ * 合并自 eshop/xtools/vcode 三项目的 security.ts,取各版本之长。
6
+ *
7
+ * 设计要点:
8
+ * - 纯函数/泛型 Context,不绑定特定项目的 AppEnv
9
+ * - 所有加密操作使用 Web Crypto API(Workers 原生)
10
+ * - IP 仅信任 cf-connecting-ip(CF 边缘注入,客户端无法伪造)
11
+ */
12
+ import type { Context } from "hono";
13
+ import type { TurnstileResult } from "./types";
14
+ /**
15
+ * 对字符串进行 SHA-256 哈希,返回 64 位十六进制字符串。
16
+ */
17
+ export declare function sha256(input: string): Promise<string>;
18
+ /**
19
+ * 恒定时间字符串比较 — 防止时序攻击。
20
+ *
21
+ * 使用 crypto.subtle.timingSafeEqual 比较两个字符串的 UTF-8 字节序列。
22
+ * 长度不匹配时回退到手写比较(仍为恒定时间),避免长度泄露信息。
23
+ *
24
+ * 来源:eshop(crypto.subtle 版)+ xtools(手写 XOR 版)合并
25
+ */
26
+ export declare function constantTimeEqual(a: string, b: string): boolean;
27
+ /**
28
+ * 获取客户端 IP 的加盐 SHA-256 哈希。
29
+ *
30
+ * - 仅信任 cf-connecting-ip(CF 边缘注入)
31
+ * - 非 CF 环境降级使用 x-forwarded-for + "dev:" 前缀
32
+ * - 加盐防止反向查找
33
+ *
34
+ * @param c - Hono Context
35
+ * @param salt - 盐值,默认从 env.RATE_LIMIT_SALT 读取
36
+ */
37
+ export declare function getIpHash(c: Context<any>, salt?: string): Promise<string>;
38
+ /**
39
+ * 获取原始客户端 IP(用于日志,不做隐私存储时可用)
40
+ */
41
+ export declare function getClientIp(c: Context): string;
42
+ /**
43
+ * 从 Authorization 请求头提取 Bearer Token。
44
+ * 格式:Authorization: Bearer <token>
45
+ */
46
+ export declare function getBearerToken(c: Context): string;
47
+ /**
48
+ * Cloudflare Turnstile 验证。
49
+ *
50
+ * - 未配置 TURNSTILE_SECRET_KEY 时直接放行(分阶段部署)
51
+ * - 无 token 时静默通过(smoke 测试/管理端调用)
52
+ * - 验证失败返回 { ok: false, message }
53
+ *
54
+ * 来源:eshop(FormData 版)+ vcode(urlencoded 版)合并为 FormData 版(更规范)
55
+ */
56
+ export declare function verifyTurnstile(c: Context<any>, token?: string): Promise<TurnstileResult>;
57
+ export interface SecurityHeadersOptions {
58
+ /** CSP 额外 script-src(如 CDN 域名) */
59
+ extraScriptSrc?: string[];
60
+ /** CSP 额外 style-src */
61
+ extraStyleSrc?: string[];
62
+ /** CSP 额外 connect-src */
63
+ extraConnectSrc?: string[];
64
+ /** CSP 额外 font-src */
65
+ extraFontSrc?: string[];
66
+ /** 是否允许 unsafe-eval(admin 页面的 Vue UMD 需要) */
67
+ allowUnsafeEval?: boolean;
68
+ /** 是否允许 Telegram WebApp SDK */
69
+ allowTelegram?: boolean;
70
+ }
71
+ /**
72
+ * 生成安全响应头(CSP + 安全头)
73
+ *
74
+ * 合并三项目不同的 CSP 策略为统一配置接口。
75
+ * 返回 Headers 对象,可直接合并到响应中。
76
+ */
77
+ export declare function buildSecurityHeaders(options?: SecurityHeadersOptions): Headers;
78
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAQ/C;;GAEG;AACH,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK3D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAU/D;AAMD;;;;;;;;;GASG;AAEH,wBAAsB,SAAS,CAC7B,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAK9C;AAMD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAIjD;AAMD;;;;;;;;GAQG;AAEH,wBAAsB,eAAe,CACnC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EACf,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAMD,MAAM,WAAW,sBAAsB;IACrC,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uBAAuB;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,yBAAyB;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sBAAsB;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,+BAA+B;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAyClF"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * 安全工具模块
3
+ *
4
+ * 提供 SHA-256 哈希、IP 哈希(加盐)、时序安全比较、Turnstile 人机验证、安全响应头。
5
+ * 合并自 eshop/xtools/vcode 三项目的 security.ts,取各版本之长。
6
+ *
7
+ * 设计要点:
8
+ * - 纯函数/泛型 Context,不绑定特定项目的 AppEnv
9
+ * - 所有加密操作使用 Web Crypto API(Workers 原生)
10
+ * - IP 仅信任 cf-connecting-ip(CF 边缘注入,客户端无法伪造)
11
+ */
12
+ const encoder = new TextEncoder();
13
+ // ═══════════════════════════════════════════════════════════════════════════════
14
+ // 基础密码学工具
15
+ // ═══════════════════════════════════════════════════════════════════════════════
16
+ /**
17
+ * 对字符串进行 SHA-256 哈希,返回 64 位十六进制字符串。
18
+ */
19
+ export async function sha256(input) {
20
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(input));
21
+ return [...new Uint8Array(digest)]
22
+ .map((b) => b.toString(16).padStart(2, "0"))
23
+ .join("");
24
+ }
25
+ /**
26
+ * 恒定时间字符串比较 — 防止时序攻击。
27
+ *
28
+ * 使用 crypto.subtle.timingSafeEqual 比较两个字符串的 UTF-8 字节序列。
29
+ * 长度不匹配时回退到手写比较(仍为恒定时间),避免长度泄露信息。
30
+ *
31
+ * 来源:eshop(crypto.subtle 版)+ xtools(手写 XOR 版)合并
32
+ */
33
+ export function constantTimeEqual(a, b) {
34
+ const aBuf = encoder.encode(a);
35
+ const bBuf = encoder.encode(b);
36
+ // 手写 XOR 比较(恒定时间,兼容 Node.js 和 Workers)
37
+ if (aBuf.byteLength !== bBuf.byteLength)
38
+ return false;
39
+ let diff = 0;
40
+ for (let i = 0; i < aBuf.byteLength; i++) {
41
+ diff |= aBuf[i] ^ bBuf[i];
42
+ }
43
+ return diff === 0;
44
+ }
45
+ // ═══════════════════════════════════════════════════════════════════════════════
46
+ // IP 哈希
47
+ // ═══════════════════════════════════════════════════════════════════════════════
48
+ /**
49
+ * 获取客户端 IP 的加盐 SHA-256 哈希。
50
+ *
51
+ * - 仅信任 cf-connecting-ip(CF 边缘注入)
52
+ * - 非 CF 环境降级使用 x-forwarded-for + "dev:" 前缀
53
+ * - 加盐防止反向查找
54
+ *
55
+ * @param c - Hono Context
56
+ * @param salt - 盐值,默认从 env.RATE_LIMIT_SALT 读取
57
+ */
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ export async function getIpHash(c, salt) {
60
+ const cfIp = c.req.header("cf-connecting-ip");
61
+ let ip;
62
+ if (cfIp) {
63
+ ip = cfIp;
64
+ }
65
+ else {
66
+ ip = "dev:" + (c.req.header("x-forwarded-for") || "0.0.0.0");
67
+ }
68
+ const actualSalt = salt ?? c.env.RATE_LIMIT_SALT ?? "cf-core-salt";
69
+ return sha256(`${actualSalt}:${ip}`);
70
+ }
71
+ /**
72
+ * 获取原始客户端 IP(用于日志,不做隐私存储时可用)
73
+ */
74
+ export function getClientIp(c) {
75
+ const cfIp = c.req.header("cf-connecting-ip");
76
+ if (cfIp)
77
+ return cfIp;
78
+ const xff = c.req.header("x-forwarded-for");
79
+ return xff ? xff.split(",")[0].trim() : "unknown";
80
+ }
81
+ // ═══════════════════════════════════════════════════════════════════════════════
82
+ // Bearer Token 提取
83
+ // ═══════════════════════════════════════════════════════════════════════════════
84
+ /**
85
+ * 从 Authorization 请求头提取 Bearer Token。
86
+ * 格式:Authorization: Bearer <token>
87
+ */
88
+ export function getBearerToken(c) {
89
+ const auth = c.req.header("authorization") || "";
90
+ const match = auth.match(/^Bearer\s+(.+)$/i);
91
+ return match?.[1]?.trim() || "";
92
+ }
93
+ // ═══════════════════════════════════════════════════════════════════════════════
94
+ // Turnstile 人机验证
95
+ // ═══════════════════════════════════════════════════════════════════════════════
96
+ /**
97
+ * Cloudflare Turnstile 验证。
98
+ *
99
+ * - 未配置 TURNSTILE_SECRET_KEY 时直接放行(分阶段部署)
100
+ * - 无 token 时静默通过(smoke 测试/管理端调用)
101
+ * - 验证失败返回 { ok: false, message }
102
+ *
103
+ * 来源:eshop(FormData 版)+ vcode(urlencoded 版)合并为 FormData 版(更规范)
104
+ */
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ export async function verifyTurnstile(c, token) {
107
+ const secret = c.env.TURNSTILE_SECRET_KEY;
108
+ if (!secret)
109
+ return { ok: true };
110
+ if (!token)
111
+ return { ok: true };
112
+ const form = new FormData();
113
+ form.append("secret", secret);
114
+ form.append("response", token);
115
+ const ip = c.req.header("cf-connecting-ip");
116
+ if (ip)
117
+ form.append("remoteip", ip);
118
+ try {
119
+ const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
120
+ method: "POST",
121
+ body: form,
122
+ });
123
+ const data = await response.json();
124
+ if (!data.success) {
125
+ console.warn("[turnstile] verification failed", {
126
+ errorCodes: data["error-codes"] || [],
127
+ });
128
+ return { ok: false, message: "人机验证失败" };
129
+ }
130
+ return { ok: true };
131
+ }
132
+ catch (err) {
133
+ console.error("[turnstile] fetch error:", err);
134
+ return { ok: true };
135
+ }
136
+ }
137
+ /**
138
+ * 生成安全响应头(CSP + 安全头)
139
+ *
140
+ * 合并三项目不同的 CSP 策略为统一配置接口。
141
+ * 返回 Headers 对象,可直接合并到响应中。
142
+ */
143
+ export function buildSecurityHeaders(options = {}) {
144
+ const { extraScriptSrc = [], extraStyleSrc = [], extraConnectSrc = [], extraFontSrc = [], allowUnsafeEval = false, allowTelegram = false, } = options;
145
+ const scriptSrc = [
146
+ "'self'",
147
+ "'unsafe-inline'",
148
+ "https://unpkg.com",
149
+ "https://static.cloudflareinsights.com",
150
+ "https://challenges.cloudflare.com",
151
+ ...(allowUnsafeEval ? ["'unsafe-eval'"] : []),
152
+ ...(allowTelegram ? ["https://telegram.org"] : []),
153
+ ...extraScriptSrc,
154
+ ];
155
+ const csp = [
156
+ "default-src 'self'",
157
+ `style-src 'self' 'unsafe-inline' https://unpkg.com ${extraStyleSrc.join(" ")}`.trim(),
158
+ `script-src ${scriptSrc.join(" ")}`,
159
+ "img-src 'self' data: https:",
160
+ `connect-src 'self' https://unpkg.com https://challenges.cloudflare.com https://static.cloudflareinsights.com ${extraConnectSrc.join(" ")}`.trim(),
161
+ "frame-src 'self' https://challenges.cloudflare.com",
162
+ "object-src 'none'",
163
+ "base-uri 'self'",
164
+ ...(extraFontSrc.length > 0 ? [`font-src 'self' ${extraFontSrc.join(" ")}`] : []),
165
+ ].join("; ");
166
+ return new Headers({
167
+ "Content-Security-Policy": csp,
168
+ "X-Content-Type-Options": "nosniff",
169
+ "X-Frame-Options": "DENY",
170
+ "Referrer-Policy": "strict-origin-when-cross-origin",
171
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()",
172
+ "Cross-Origin-Opener-Policy": "same-origin",
173
+ });
174
+ }
175
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @usethink/cf-core — 公共类型定义
3
+ *
4
+ * 所有项目共享的基础类型约束。
5
+ * 各项目通过 extends 扩展自己的 AppEnv,保持与 cf-core 兼容。
6
+ */
7
+ /**
8
+ * 所有 Cloudflare Workers 项目必须满足的最小 Bindings 约束。
9
+ * 各项目在此基础上添加自己的环境变量。
10
+ */
11
+ export interface CoreBindings {
12
+ TURSO_URL?: string;
13
+ TURSO_TOKEN?: string;
14
+ ADMIN_TOKEN?: string;
15
+ TURNSTILE_SECRET_KEY?: string;
16
+ RATE_LIMIT_SALT?: string;
17
+ APP_ORIGIN?: string;
18
+ }
19
+ /**
20
+ * Hono Variables 的最小约束。
21
+ * 各项目的 Variables 至少包含 db 字段,类型由项目自行指定。
22
+ */
23
+ export interface CoreVariables {
24
+ db: unknown;
25
+ }
26
+ /**
27
+ * Hono 上下文的最小环境约束。
28
+ * cf-core 中所有需要 Context 的函数都以此为泛型上界。
29
+ */
30
+ export interface CoreEnv {
31
+ Bindings: CoreBindings;
32
+ Variables: CoreVariables;
33
+ }
34
+ /**
35
+ * ok/fail 统一响应格式。
36
+ */
37
+ export interface OkResponse {
38
+ ok: true;
39
+ [key: string]: unknown;
40
+ }
41
+ export interface FailResponse {
42
+ ok: false;
43
+ error: string;
44
+ details?: unknown;
45
+ }
46
+ /**
47
+ * Turnstile 验证结果。
48
+ */
49
+ export interface TurnstileResult {
50
+ ok: boolean;
51
+ message?: string;
52
+ }
53
+ /**
54
+ * 限流检查结果。
55
+ */
56
+ export interface RateLimitResult {
57
+ ok: boolean;
58
+ message?: string;
59
+ status?: number;
60
+ ipHash?: string;
61
+ remaining?: number;
62
+ resetMs?: number;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,aAAa,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @usethink/cf-core — 公共类型定义
3
+ *
4
+ * 所有项目共享的基础类型约束。
5
+ * 各项目通过 extends 扩展自己的 AppEnv,保持与 cf-core 兼容。
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * anti-abuse — 防刷检测插件
3
+ *
4
+ * 提供 API 级别的异常行为检测:
5
+ * - 多 IP 访问同一资源
6
+ * - 高频请求检测
7
+ * - 凭证扫描检测(顺序遍历 ID)
8
+ * - 快速连续操作检测
9
+ *
10
+ * 纯内存实现(Workers 实例级别),无需外部存储。
11
+ *
12
+ * 来源:vcode src/services/anti-abuse.ts
13
+ */
14
+
15
+ // ═══════════════════════════════════════════════════════════════════════════════
16
+ // 类型定义
17
+ // ═══════════════════════════════════════════════════════════════════════════════
18
+
19
+ export type AbuseType = "multi_ip_access" | "high_frequency" | "token_scan" | "rapid_fire";
20
+
21
+ export interface AbuseEvent {
22
+ type: AbuseType;
23
+ resourceId: string;
24
+ ipHash: string;
25
+ timestamp: number;
26
+ detail: string;
27
+ }
28
+
29
+ export interface AntiAbuseConfig {
30
+ /** 同一资源允许的最大不同 IP 数(默认 5) */
31
+ maxIpsPerResource?: number;
32
+ /** 同一 IP 在窗口内允许的最大请求数(默认 30) */
33
+ maxRequestsPerIp?: number;
34
+ /** 请求频率窗口(毫秒,默认 60000) */
35
+ windowMs?: number;
36
+ /** 连续 ID 差值阈值(检测扫描,默认 3) */
37
+ scanThreshold?: number;
38
+ /** 快速连续操作最小间隔(毫秒,默认 500) */
39
+ rapidFireIntervalMs?: number;
40
+ }
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════════════
43
+ // AntiAbuseService
44
+ // ═══════════════════════════════════════════════════════════════════════════════
45
+
46
+ export class AntiAbuseService {
47
+ readonly name = "anti-abuse";
48
+ readonly version = "0.1.0";
49
+
50
+ private config: Required<AntiAbuseConfig>;
51
+
52
+ // 内存存储
53
+ private resourceIps = new Map<string, Set<string>>();
54
+ private ipRequests = new Map<string, number[]>();
55
+ private lastAccessByIp = new Map<string, number>();
56
+ private lastResourceIdByIp = new Map<string, string>();
57
+ private events: AbuseEvent[] = [];
58
+
59
+ constructor(config: AntiAbuseConfig = {}) {
60
+ this.config = {
61
+ maxIpsPerResource: config.maxIpsPerResource ?? 5,
62
+ maxRequestsPerIp: config.maxRequestsPerIp ?? 30,
63
+ windowMs: config.windowMs ?? 60_000,
64
+ scanThreshold: config.scanThreshold ?? 3,
65
+ rapidFireIntervalMs: config.rapidFireIntervalMs ?? 500,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * 记录一次资源访问,返回检测到的异常事件列表
71
+ */
72
+ record(resourceId: string, ipHash: string): AbuseEvent[] {
73
+ const now = Date.now();
74
+ const detected: AbuseEvent[] = [];
75
+
76
+ // 1. 多 IP 访问检测
77
+ if (!this.resourceIps.has(resourceId)) {
78
+ this.resourceIps.set(resourceId, new Set());
79
+ }
80
+ const ips = this.resourceIps.get(resourceId)!;
81
+ ips.add(ipHash);
82
+ if (ips.size > this.config.maxIpsPerResource) {
83
+ detected.push({
84
+ type: "multi_ip_access",
85
+ resourceId,
86
+ ipHash,
87
+ timestamp: now,
88
+ detail: `${ips.size} different IPs accessed this resource (limit: ${this.config.maxIpsPerResource})`,
89
+ });
90
+ }
91
+
92
+ // 2. 高频请求检测
93
+ if (!this.ipRequests.has(ipHash)) {
94
+ this.ipRequests.set(ipHash, []);
95
+ }
96
+ const timestamps = this.ipRequests.get(ipHash)!;
97
+ const windowStart = now - this.config.windowMs;
98
+ const validTimestamps = timestamps.filter((ts) => ts > windowStart);
99
+ validTimestamps.push(now);
100
+ this.ipRequests.set(ipHash, validTimestamps);
101
+
102
+ if (validTimestamps.length > this.config.maxRequestsPerIp) {
103
+ detected.push({
104
+ type: "high_frequency",
105
+ resourceId,
106
+ ipHash,
107
+ timestamp: now,
108
+ detail: `${validTimestamps.length} requests in ${this.config.windowMs}ms (limit: ${this.config.maxRequestsPerIp})`,
109
+ });
110
+ }
111
+
112
+ // 3. 凭证扫描检测
113
+ const lastId = this.lastResourceIdByIp.get(ipHash);
114
+ if (lastId) {
115
+ const idDiff = this.parseNumericId(resourceId) - this.parseNumericId(lastId);
116
+ if (idDiff > 0 && idDiff <= this.config.scanThreshold) {
117
+ detected.push({
118
+ type: "token_scan",
119
+ resourceId,
120
+ ipHash,
121
+ timestamp: now,
122
+ detail: `Sequential ID access: ${lastId} → ${resourceId} (diff: ${idDiff})`,
123
+ });
124
+ }
125
+ }
126
+ this.lastResourceIdByIp.set(ipHash, resourceId);
127
+
128
+ // 4. 快速连续操作检测
129
+ const lastAccess = this.lastAccessByIp.get(ipHash);
130
+ if (lastAccess && now - lastAccess < this.config.rapidFireIntervalMs) {
131
+ detected.push({
132
+ type: "rapid_fire",
133
+ resourceId,
134
+ ipHash,
135
+ timestamp: now,
136
+ detail: `Rapid access: ${now - lastAccess}ms (min: ${this.config.rapidFireIntervalMs}ms)`,
137
+ });
138
+ }
139
+ this.lastAccessByIp.set(ipHash, now);
140
+
141
+ // 记录事件
142
+ for (const event of detected) {
143
+ this.events.push(event);
144
+ }
145
+
146
+ return detected;
147
+ }
148
+
149
+ /**
150
+ * 获取最近的异常事件
151
+ */
152
+ getEvents(limit = 50): AbuseEvent[] {
153
+ return this.events.slice(-limit);
154
+ }
155
+
156
+ /**
157
+ * 检查某个 IP 是否可疑
158
+ */
159
+ isSuspicious(ipHash: string): boolean {
160
+ return this.events.some((e) => e.ipHash === ipHash);
161
+ }
162
+
163
+ /**
164
+ * 清理过期数据
165
+ */
166
+ cleanup(): void {
167
+ const cutoff = Date.now() - this.config.windowMs * 2;
168
+ for (const [ip, timestamps] of this.ipRequests) {
169
+ const valid = timestamps.filter((ts) => ts > cutoff);
170
+ if (valid.length === 0) this.ipRequests.delete(ip);
171
+ else this.ipRequests.set(ip, valid);
172
+ }
173
+ this.events = this.events.filter((e) => e.timestamp > cutoff);
174
+ }
175
+
176
+ private parseNumericId(id: string): number {
177
+ const match = id.match(/(\d+)$/);
178
+ return match ? parseInt(match[1]) : 0;
179
+ }
180
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AntiAbuseService } from "../index";
3
+
4
+ describe("AntiAbuseService", () => {
5
+ it("正常访问无异常", () => {
6
+ const svc = new AntiAbuseService();
7
+ const events = svc.record("token-abc-123", "ip-hash-1");
8
+ expect(events).toEqual([]);
9
+ });
10
+
11
+ it("高频请求检测", () => {
12
+ const svc = new AntiAbuseService({ maxRequestsPerIp: 3, windowMs: 60000 });
13
+ let events: any[] = [];
14
+ for (let i = 0; i < 5; i++) {
15
+ events = svc.record(`token-${i}`, "same-ip");
16
+ }
17
+ expect(events.some((e) => e.type === "high_frequency")).toBe(true);
18
+ });
19
+
20
+ it("多 IP 访问检测", () => {
21
+ const svc = new AntiAbuseService({ maxIpsPerResource: 2 });
22
+ svc.record("token-shared", "ip-1");
23
+ svc.record("token-shared", "ip-2");
24
+ const events = svc.record("token-shared", "ip-3");
25
+ expect(events.some((e) => e.type === "multi_ip_access")).toBe(true);
26
+ });
27
+
28
+ it("快速连续操作检测", () => {
29
+ const svc = new AntiAbuseService({ rapidFireIntervalMs: 1000 });
30
+ svc.record("token-a", "fast-ip");
31
+ // 立即再次访问
32
+ const events = svc.record("token-b", "fast-ip");
33
+ expect(events.some((e) => e.type === "rapid_fire")).toBe(true);
34
+ });
35
+
36
+ it("isSuspicious 检测", () => {
37
+ const svc = new AntiAbuseService({ maxRequestsPerIp: 1 });
38
+ svc.record("t-1", "bad-ip");
39
+ svc.record("t-2", "bad-ip"); // 触发 high_frequency
40
+ expect(svc.isSuspicious("bad-ip")).toBe(true);
41
+ expect(svc.isSuspicious("clean-ip")).toBe(false);
42
+ });
43
+
44
+ it("cleanup 清理过期数据", () => {
45
+ const svc = new AntiAbuseService({ windowMs: 1 });
46
+ svc.record("t-1", "ip-1");
47
+ svc.cleanup();
48
+ expect(svc.getEvents()).toEqual([]);
49
+ });
50
+ });