@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.
- package/README.md +88 -0
- package/dist/features/anti-abuse/index.d.ts +62 -0
- package/dist/features/anti-abuse/index.d.ts.map +1 -0
- package/dist/features/anti-abuse/index.js +139 -0
- package/dist/features/anti-abuse/index.js.map +1 -0
- package/dist/features/email/index.d.ts +68 -0
- package/dist/features/email/index.d.ts.map +1 -0
- package/dist/features/email/index.js +120 -0
- package/dist/features/email/index.js.map +1 -0
- package/dist/features/payment/fetch-utils.d.ts +17 -0
- package/dist/features/payment/fetch-utils.d.ts.map +1 -0
- package/dist/features/payment/fetch-utils.js +56 -0
- package/dist/features/payment/fetch-utils.js.map +1 -0
- package/dist/features/payment/index.d.ts +20 -0
- package/dist/features/payment/index.d.ts.map +1 -0
- package/dist/features/payment/index.js +21 -0
- package/dist/features/payment/index.js.map +1 -0
- package/dist/features/payment/providers/alipay.d.ts +30 -0
- package/dist/features/payment/providers/alipay.d.ts.map +1 -0
- package/dist/features/payment/providers/alipay.js +149 -0
- package/dist/features/payment/providers/alipay.js.map +1 -0
- package/dist/features/payment/providers/stripe.d.ts +34 -0
- package/dist/features/payment/providers/stripe.d.ts.map +1 -0
- package/dist/features/payment/providers/stripe.js +168 -0
- package/dist/features/payment/providers/stripe.js.map +1 -0
- package/dist/features/payment/providers/trc20.d.ts +24 -0
- package/dist/features/payment/providers/trc20.d.ts.map +1 -0
- package/dist/features/payment/providers/trc20.js +96 -0
- package/dist/features/payment/providers/trc20.js.map +1 -0
- package/dist/features/payment/registry.d.ts +43 -0
- package/dist/features/payment/registry.d.ts.map +1 -0
- package/dist/features/payment/registry.js +65 -0
- package/dist/features/payment/registry.js.map +1 -0
- package/dist/features/payment/types.d.ts +72 -0
- package/dist/features/payment/types.d.ts.map +1 -0
- package/dist/features/payment/types.js +8 -0
- package/dist/features/payment/types.js.map +1 -0
- package/dist/features/thompson-router/index.d.ts +101 -0
- package/dist/features/thompson-router/index.d.ts.map +1 -0
- package/dist/features/thompson-router/index.js +186 -0
- package/dist/features/thompson-router/index.js.map +1 -0
- package/dist/features/webhook/index.d.ts +76 -0
- package/dist/features/webhook/index.d.ts.map +1 -0
- package/dist/features/webhook/index.js +127 -0
- package/dist/features/webhook/index.js.map +1 -0
- package/dist/src/audit.d.ts +45 -0
- package/dist/src/audit.d.ts.map +1 -0
- package/dist/src/audit.js +40 -0
- package/dist/src/audit.js.map +1 -0
- package/dist/src/auth/jwt.d.ts +33 -0
- package/dist/src/auth/jwt.d.ts.map +1 -0
- package/dist/src/auth/jwt.js +87 -0
- package/dist/src/auth/jwt.js.map +1 -0
- package/dist/src/auth/password.d.ts +26 -0
- package/dist/src/auth/password.d.ts.map +1 -0
- package/dist/src/auth/password.js +52 -0
- package/dist/src/auth/password.js.map +1 -0
- package/dist/src/bootstrap.d.ts +74 -0
- package/dist/src/bootstrap.d.ts.map +1 -0
- package/dist/src/bootstrap.js +231 -0
- package/dist/src/bootstrap.js.map +1 -0
- package/dist/src/cache.d.ts +52 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +76 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +96 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/crypto.d.ts +33 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +87 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/db/connection.d.ts +53 -0
- package/dist/src/db/connection.d.ts.map +1 -0
- package/dist/src/db/connection.js +104 -0
- package/dist/src/db/connection.js.map +1 -0
- package/dist/src/db/index.d.ts +6 -0
- package/dist/src/db/index.d.ts.map +1 -0
- package/dist/src/db/index.js +6 -0
- package/dist/src/db/index.js.map +1 -0
- package/dist/src/db/schema.d.ts +649 -0
- package/dist/src/db/schema.d.ts.map +1 -0
- package/dist/src/db/schema.js +76 -0
- package/dist/src/db/schema.js.map +1 -0
- package/dist/src/error.d.ts +47 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +94 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/http.d.ts +83 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +116 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/idempotency.d.ts +78 -0
- package/dist/src/idempotency.d.ts.map +1 -0
- package/dist/src/idempotency.js +84 -0
- package/dist/src/idempotency.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +45 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.d.ts +31 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/middleware/admin-auth.d.ts +38 -0
- package/dist/src/middleware/admin-auth.d.ts.map +1 -0
- package/dist/src/middleware/admin-auth.js +55 -0
- package/dist/src/middleware/admin-auth.js.map +1 -0
- package/dist/src/middleware/api-key-auth.d.ts +42 -0
- package/dist/src/middleware/api-key-auth.d.ts.map +1 -0
- package/dist/src/middleware/api-key-auth.js +104 -0
- package/dist/src/middleware/api-key-auth.js.map +1 -0
- package/dist/src/middleware/index.d.ts +3 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/index.js +3 -0
- package/dist/src/middleware/index.js.map +1 -0
- package/dist/src/rate-limit.d.ts +54 -0
- package/dist/src/rate-limit.d.ts.map +1 -0
- package/dist/src/rate-limit.js +134 -0
- package/dist/src/rate-limit.js.map +1 -0
- package/dist/src/security.d.ts +78 -0
- package/dist/src/security.d.ts.map +1 -0
- package/dist/src/security.js +175 -0
- package/dist/src/security.js.map +1 -0
- package/dist/src/types.d.ts +64 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -0
- package/features/anti-abuse/index.ts +180 -0
- package/features/anti-abuse/tests/index.test.ts +50 -0
- package/features/email/index.ts +172 -0
- package/features/email/tests/index.test.ts +44 -0
- package/features/payment/fetch-utils.ts +65 -0
- package/features/payment/index.ts +39 -0
- package/features/payment/providers/alipay.ts +171 -0
- package/features/payment/providers/stripe.ts +192 -0
- package/features/payment/providers/trc20.ts +115 -0
- package/features/payment/registry.ts +87 -0
- package/features/payment/tests/index.test.ts +506 -0
- package/features/payment/types.ts +93 -0
- package/features/telegram-miniapp/index.ts +109 -0
- package/features/telegram-miniapp/tests/index.test.ts +11 -0
- package/features/thompson-router/index.ts +243 -0
- package/features/thompson-router/tests/index.test.ts +93 -0
- package/features/webhook/index.ts +183 -0
- package/features/webhook/tests/index.test.ts +21 -0
- package/package.json +202 -0
- package/src/audit.ts +70 -0
- package/src/auth/jwt.ts +114 -0
- package/src/auth/password.ts +75 -0
- package/src/bootstrap.ts +322 -0
- package/src/cache.ts +78 -0
- package/src/config.ts +134 -0
- package/src/crypto.ts +106 -0
- package/src/db/connection.ts +127 -0
- package/src/db/index.ts +6 -0
- package/src/db/schema.ts +90 -0
- package/src/error.ts +125 -0
- package/src/http.ts +150 -0
- package/src/idempotency.ts +127 -0
- package/src/index.ts +85 -0
- package/src/logger.ts +63 -0
- package/src/middleware/admin-auth.ts +71 -0
- package/src/middleware/api-key-auth.ts +164 -0
- package/src/middleware/index.ts +2 -0
- package/src/rate-limit.ts +167 -0
- package/src/security.ts +219 -0
- 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
|
+
}
|
package/src/auth/jwt.ts
ADDED
|
@@ -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
|
+
}
|