@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
package/package.json ADDED
@@ -0,0 +1,202 @@
1
+ {
2
+ "name": "@usethink/cf-core",
3
+ "version": "0.3.0",
4
+ "description": "Cloudflare Workers 共享内核 — Hono + Turso + Drizzle 标准化基础设施",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "types": "dist/src/index.d.ts",
8
+ "files": [
9
+ "dist/src",
10
+ "dist/features",
11
+ "src",
12
+ "features",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "registry": "https://registry.npmjs.org/"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/src/index.d.ts",
22
+ "import": {
23
+ "development": "./src/index.ts",
24
+ "default": "./dist/src/index.js"
25
+ }
26
+ },
27
+ "./types": {
28
+ "types": "./dist/src/types.d.ts",
29
+ "import": {
30
+ "development": "./src/types.ts",
31
+ "default": "./dist/src/types.js"
32
+ }
33
+ },
34
+ "./http": {
35
+ "types": "./dist/src/http.d.ts",
36
+ "import": {
37
+ "development": "./src/http.ts",
38
+ "default": "./dist/src/http.js"
39
+ }
40
+ },
41
+ "./security": {
42
+ "types": "./dist/src/security.d.ts",
43
+ "import": {
44
+ "development": "./src/security.ts",
45
+ "default": "./dist/src/security.js"
46
+ }
47
+ },
48
+ "./rate-limit": {
49
+ "types": "./dist/src/rate-limit.d.ts",
50
+ "import": {
51
+ "development": "./src/rate-limit.ts",
52
+ "default": "./dist/src/rate-limit.js"
53
+ }
54
+ },
55
+ "./cache": {
56
+ "types": "./dist/src/cache.d.ts",
57
+ "import": {
58
+ "development": "./src/cache.ts",
59
+ "default": "./dist/src/cache.js"
60
+ }
61
+ },
62
+ "./idempotency": {
63
+ "types": "./dist/src/idempotency.d.ts",
64
+ "import": {
65
+ "development": "./src/idempotency.ts",
66
+ "default": "./dist/src/idempotency.js"
67
+ }
68
+ },
69
+ "./audit": {
70
+ "types": "./dist/src/audit.d.ts",
71
+ "import": {
72
+ "development": "./src/audit.ts",
73
+ "default": "./dist/src/audit.js"
74
+ }
75
+ },
76
+ "./config": {
77
+ "types": "./dist/src/config.d.ts",
78
+ "import": {
79
+ "development": "./src/config.ts",
80
+ "default": "./dist/src/config.js"
81
+ }
82
+ },
83
+ "./bootstrap": {
84
+ "types": "./dist/src/bootstrap.d.ts",
85
+ "import": {
86
+ "development": "./src/bootstrap.ts",
87
+ "default": "./dist/src/bootstrap.js"
88
+ }
89
+ },
90
+ "./error": {
91
+ "types": "./dist/src/error.d.ts",
92
+ "import": {
93
+ "development": "./src/error.ts",
94
+ "default": "./dist/src/error.js"
95
+ }
96
+ },
97
+ "./logger": {
98
+ "types": "./dist/src/logger.d.ts",
99
+ "import": {
100
+ "development": "./src/logger.ts",
101
+ "default": "./dist/src/logger.js"
102
+ }
103
+ },
104
+ "./crypto": {
105
+ "types": "./dist/src/crypto.d.ts",
106
+ "import": {
107
+ "development": "./src/crypto.ts",
108
+ "default": "./dist/src/crypto.js"
109
+ }
110
+ },
111
+ "./db": {
112
+ "types": "./dist/src/db/index.d.ts",
113
+ "import": {
114
+ "development": "./src/db/index.ts",
115
+ "default": "./dist/src/db/index.js"
116
+ }
117
+ },
118
+ "./db/schema": {
119
+ "types": "./dist/src/db/schema.d.ts",
120
+ "import": {
121
+ "development": "./src/db/schema.ts",
122
+ "default": "./dist/src/db/schema.js"
123
+ }
124
+ },
125
+ "./auth/jwt": {
126
+ "types": "./dist/src/auth/jwt.d.ts",
127
+ "import": {
128
+ "development": "./src/auth/jwt.ts",
129
+ "default": "./dist/src/auth/jwt.js"
130
+ }
131
+ },
132
+ "./auth/password": {
133
+ "types": "./dist/src/auth/password.d.ts",
134
+ "import": {
135
+ "development": "./src/auth/password.ts",
136
+ "default": "./dist/src/auth/password.js"
137
+ }
138
+ },
139
+ "./middleware": {
140
+ "types": "./dist/src/middleware/index.d.ts",
141
+ "import": {
142
+ "development": "./src/middleware/index.ts",
143
+ "default": "./dist/src/middleware/index.js"
144
+ }
145
+ },
146
+ "./features/payment": {
147
+ "types": "./dist/features/payment/index.d.ts",
148
+ "import": {
149
+ "development": "./features/payment/index.ts",
150
+ "default": "./dist/features/payment/index.js"
151
+ }
152
+ },
153
+ "./features/email": {
154
+ "types": "./dist/features/email/index.d.ts",
155
+ "import": {
156
+ "development": "./features/email/index.ts",
157
+ "default": "./dist/features/email/index.js"
158
+ }
159
+ },
160
+ "./features/webhook": {
161
+ "types": "./dist/features/webhook/index.d.ts",
162
+ "import": {
163
+ "development": "./features/webhook/index.ts",
164
+ "default": "./dist/features/webhook/index.js"
165
+ }
166
+ },
167
+ "./features/thompson-router": {
168
+ "types": "./dist/features/thompson-router/index.d.ts",
169
+ "import": {
170
+ "development": "./features/thompson-router/index.ts",
171
+ "default": "./dist/features/thompson-router/index.js"
172
+ }
173
+ },
174
+ "./features/anti-abuse": {
175
+ "types": "./dist/features/anti-abuse/index.d.ts",
176
+ "import": {
177
+ "development": "./features/anti-abuse/index.ts",
178
+ "default": "./dist/features/anti-abuse/index.js"
179
+ }
180
+ }
181
+ },
182
+ "scripts": {
183
+ "build": "tsc -p tsconfig.build.json",
184
+ "type-check": "tsc --noEmit",
185
+ "test": "vitest run",
186
+ "test:watch": "vitest",
187
+ "prepublishOnly": "npm run type-check && npm test",
188
+ "prepack": "npm run build"
189
+ },
190
+ "dependencies": {
191
+ "@libsql/client": "^0.14.0",
192
+ "drizzle-orm": "^0.45.2",
193
+ "drizzle-zod": "^0.7.0",
194
+ "hono": "^4.12.22",
195
+ "zod": "^3.23.0"
196
+ },
197
+ "devDependencies": {
198
+ "@cloudflare/workers-types": "^4.20260524.1",
199
+ "typescript": "^5.7.0",
200
+ "vitest": "^2.1.0"
201
+ }
202
+ }
package/src/audit.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 审计日志模块
3
+ *
4
+ * 提供管理员操作审计和通用事件日志。
5
+ * 设计为 fire-and-forget:写入失败不阻塞主流程。
6
+ * 内置 5% 概率自动清理旧日志,避免数据无限增长。
7
+ *
8
+ * 来源:eshop/vcode audit-service.ts 合并
9
+ */
10
+
11
+ import { adminAuditLogs } from "./db/schema";
12
+
13
+ /**
14
+ * 通用 Drizzle 数据库接口(仅约束审计模块需要的方法)
15
+ */
16
+ interface AuditDbLike {
17
+ insert: (table: typeof adminAuditLogs) => {
18
+ values: (data: {
19
+ id: string;
20
+ action: string;
21
+ targetType: string;
22
+ targetId: string;
23
+ metadataJson: string;
24
+ ipHash: string;
25
+ createdAt: string;
26
+ }) => Promise<unknown>;
27
+ };
28
+ $client?: {
29
+ execute: (sql: string) => Promise<unknown>;
30
+ };
31
+ }
32
+
33
+ export interface AuditInput {
34
+ action: string;
35
+ targetType?: string;
36
+ targetId?: string;
37
+ metadata?: unknown;
38
+ ipHash?: string;
39
+ }
40
+
41
+ /**
42
+ * 写入管理员审计日志
43
+ *
44
+ * fire-and-forget 模式:调用方应使用 ctx.waitUntil() 或直接 await。
45
+ * 写入失败仅打印 warn,不抛异常。
46
+ */
47
+ export async function writeAdminAudit(db: AuditDbLike, input: AuditInput): Promise<void> {
48
+ try {
49
+ await db.insert(adminAuditLogs).values({
50
+ id: crypto.randomUUID(),
51
+ action: input.action,
52
+ targetType: input.targetType || "",
53
+ targetId: input.targetId || "",
54
+ metadataJson: JSON.stringify(input.metadata || {}),
55
+ ipHash: input.ipHash || "",
56
+ createdAt: new Date().toISOString(),
57
+ });
58
+
59
+ // 5% 概率清理超过 90 天的旧日志
60
+ if (Math.random() < 0.05) {
61
+ try {
62
+ db.$client?.execute(
63
+ `DELETE FROM admin_audit_logs WHERE created_at < datetime('now', '-90 days')`,
64
+ ).catch(() => {});
65
+ } catch { /* mock DB may not have execute */ }
66
+ }
67
+ } catch (err) {
68
+ console.warn("[audit] writeAdminAudit failed:", err instanceof Error ? err.message : String(err));
69
+ }
70
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * JWT 签发与验证(HMAC-SHA256)
3
+ *
4
+ * 纯 Web Crypto API 实现,零外部依赖。
5
+ * Workers 原生支持,性能优异。
6
+ *
7
+ * 来源:xtools src/lib/auth.ts
8
+ */
9
+
10
+ export interface JwtPayload {
11
+ sub: string;
12
+ email: string;
13
+ iat: number;
14
+ exp: number;
15
+ }
16
+
17
+ const DEFAULT_EXPIRY = 24 * 60 * 60; // 24 小时
18
+
19
+ function base64UrlEncode(data: Uint8Array | string): string {
20
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
21
+ let binary = "";
22
+ for (const b of bytes) binary += String.fromCharCode(b);
23
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
24
+ }
25
+
26
+ function base64UrlDecode(str: string): Uint8Array {
27
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
28
+ const padding = 4 - (base64.length % 4);
29
+ if (padding !== 4) base64 += "=".repeat(padding);
30
+ const binary = atob(base64);
31
+ const bytes = new Uint8Array(binary.length);
32
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
33
+ return bytes;
34
+ }
35
+
36
+ async function importHmacKey(secret: string): Promise<CryptoKey> {
37
+ return crypto.subtle.importKey(
38
+ "raw",
39
+ new TextEncoder().encode(secret),
40
+ { name: "HMAC", hash: "SHA-256" },
41
+ false,
42
+ ["sign", "verify"],
43
+ );
44
+ }
45
+
46
+ /**
47
+ * 签发 JWT
48
+ */
49
+ export async function signJwt(
50
+ userId: string,
51
+ email: string,
52
+ secret: string,
53
+ expirySeconds = DEFAULT_EXPIRY,
54
+ ): Promise<string> {
55
+ const now = Math.floor(Date.now() / 1000);
56
+ const payload: JwtPayload = { sub: userId, email, iat: now, exp: now + expirySeconds };
57
+
58
+ const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
59
+ const body = base64UrlEncode(JSON.stringify(payload));
60
+ const signingInput = `${header}.${body}`;
61
+
62
+ const key = await importHmacKey(secret);
63
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signingInput));
64
+
65
+ return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
66
+ }
67
+
68
+ /**
69
+ * 验证并解析 JWT — 验证失败返回 null
70
+ */
71
+ export async function verifyJwt(token: string, secret: string): Promise<JwtPayload | null> {
72
+ try {
73
+ const parts = token.split(".");
74
+ if (parts.length !== 3) return null;
75
+
76
+ const [header, body, signature] = parts;
77
+ const signingInput = `${header}.${body}`;
78
+
79
+ const key = await importHmacKey(secret);
80
+ const valid = await crypto.subtle.verify(
81
+ "HMAC",
82
+ key,
83
+ base64UrlDecode(signature),
84
+ new TextEncoder().encode(signingInput),
85
+ );
86
+ if (!valid) return null;
87
+
88
+ const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body))) as JwtPayload;
89
+ if (payload.exp < Math.floor(Date.now() / 1000)) return null;
90
+
91
+ return payload;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 从请求中提取 JWT Token
99
+ *
100
+ * 优先级:Authorization: Bearer > Cookie: token=
101
+ */
102
+ export function extractJwt(c: { req: { header: (name: string) => string | undefined } }): string | null {
103
+ const auth = c.req.header("Authorization");
104
+ if (auth?.startsWith("Bearer ")) {
105
+ const token = auth.slice(7).trim();
106
+ if (token.split(".").length === 3) return token;
107
+ }
108
+ const cookie = c.req.header("Cookie");
109
+ if (cookie) {
110
+ const match = cookie.match(/(?:^|;\s*)token=([^;]+)/);
111
+ if (match) return decodeURIComponent(match[1]);
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 密码哈希模块(PBKDF2-HMAC-SHA256)
3
+ *
4
+ * 纯 Web Crypto API 实现,零外部依赖。
5
+ * - 100,000 次迭代(OWASP 推荐最低值)
6
+ * - 16 字节随机 salt
7
+ * - 32 字节(256 bit)哈希输出
8
+ *
9
+ * 来源:xtools src/lib/auth.ts
10
+ */
11
+
12
+ const ITERATIONS = 100_000;
13
+ const SALT_LENGTH = 16;
14
+ const HASH_LENGTH = 32;
15
+
16
+ function toHex(bytes: Uint8Array): string {
17
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
18
+ }
19
+
20
+ function fromHex(hex: string): Uint8Array {
21
+ const bytes = new Uint8Array(hex.length / 2);
22
+ for (let i = 0; i < hex.length; i += 2) {
23
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
24
+ }
25
+ return bytes;
26
+ }
27
+
28
+ /**
29
+ * 哈希密码
30
+ *
31
+ * @param password - 明文密码
32
+ * @param saltHex - 盐值 hex(可选,不提供则自动生成)
33
+ * @returns { hash, salt } — 均为 hex 字符串
34
+ */
35
+ export async function hashPassword(
36
+ password: string,
37
+ saltHex?: string,
38
+ ): Promise<{ hash: string; salt: string }> {
39
+ const salt = saltHex
40
+ ? fromHex(saltHex)
41
+ : crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
42
+
43
+ const keyMaterial = await crypto.subtle.importKey(
44
+ "raw",
45
+ new TextEncoder().encode(password),
46
+ "PBKDF2",
47
+ false,
48
+ ["deriveBits"],
49
+ );
50
+
51
+ const hashBuffer = await crypto.subtle.deriveBits(
52
+ { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
53
+ keyMaterial,
54
+ HASH_LENGTH * 8,
55
+ );
56
+
57
+ return { hash: toHex(new Uint8Array(hashBuffer)), salt: toHex(salt) };
58
+ }
59
+
60
+ /**
61
+ * 验证密码 — 恒定时间比较(防 timing attack)
62
+ */
63
+ export async function verifyPassword(
64
+ password: string,
65
+ hashHex: string,
66
+ saltHex: string,
67
+ ): Promise<boolean> {
68
+ const result = await hashPassword(password, saltHex);
69
+ if (result.hash.length !== hashHex.length) return false;
70
+ let diff = 0;
71
+ for (let i = 0; i < result.hash.length; i++) {
72
+ diff |= result.hash.charCodeAt(i) ^ hashHex.charCodeAt(i);
73
+ }
74
+ return diff === 0;
75
+ }