@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,506 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ createProviderRegistry,
4
+ AlipayProvider,
5
+ alipayFactory,
6
+ StripeProvider,
7
+ stripeFactory,
8
+ Trc20Provider,
9
+ trc20Factory,
10
+ signRSA2,
11
+ verifyRSA2,
12
+ } from "../index";
13
+ import type { ProviderFactory, PaymentProvider } from "../index";
14
+
15
+ // ── 测试用 RSA 密钥对(2048-bit,仅用于测试) ──
16
+ const TEST_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
17
+ MIIEpAIBAAKCAQEA2a2rwplBQLHgHnYj+EXqSM3Ww8kOzE5LIQpMbrkHMLTq8R+
18
+ S6mNxDfOB4e7jLRn7bVB3G9CLfHVE+Z4e2rVWOCmWbqS8J4pRiVf11mZl8aG9b89
19
+ O0L6VfK5jH2G1KxY8b0qR7F5u3n5p0S4yK7dX5cW6vH3kM7nL5gN5jP9rQ0xT5u
20
+ R8vK2wN8jL5hP7rT3xU6uS9wL1jM5iO7kR2hT8uV4xY0vN3jK6hP9rT5xU8uS1w
21
+ M4jN6iO8kR3hT9uV5xY1vN4jK7hP0rT6xU9uS2wM5jN7iO9kR4hT0uV6xY2vN5j
22
+ K8hP1rT7xU0vS3wM6jN8iO0kR5hT1uV7xY3vN6jK9hP2rT8xU1vS4wM7jN9iO1kR
23
+ 6hT2uV8xY4vN7jK0hP3rT9xU2vS5wM8jN0iO2kR7hT3uV9xY5vN8jK1hP4rT0xU
24
+ 3vS6wM9jN1iO3kR8hT4uV0xY6vN9jK2hP5rT1xU4vS7wM0jN2iO4kR9hT5uV1xY
25
+ 7vN0jK3hP6rT2xU5vS8wM1jN3iO5kR0hT6uV2xY8vN1jK4hP7rT3xU6vS9wIDAQAB
26
+ AoIBAC5RgZ+hBx7xHNaMpqnnfXPlM7rEsWhG2CjQIFQh9BFY6F6jC6qLOi9vY3u
27
+ 8NvY+8YjJnM2Z8TbP+GnT7aR7xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5
28
+ G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF
29
+ 8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4v
30
+ D3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J
31
+ +L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+
32
+ 5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF
33
+ 8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4vD3PqF8xH+5G7J+L4v
34
+ -----END RSA PRIVATE KEY-----`;
35
+
36
+ // 使用 Web Crypto API 生成真实测试密钥对
37
+ async function generateTestKeys() {
38
+ const keyPair = await crypto.subtle.generateKey(
39
+ { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
40
+ true,
41
+ ["sign", "verify"],
42
+ );
43
+ const privDer = new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey));
44
+ const pubDer = new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey));
45
+
46
+ function toBase64(arr: Uint8Array) {
47
+ let s = "";
48
+ for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
49
+ return btoa(s);
50
+ }
51
+
52
+ const privPem = `-----BEGIN RSA PRIVATE KEY-----\n${toBase64(privDer)}\n-----END RSA PRIVATE KEY-----`;
53
+ const pubPem = `-----BEGIN RSA PUBLIC KEY-----\n${toBase64(pubDer)}\n-----END RSA PUBLIC KEY-----`;
54
+ return { privPem, pubPem };
55
+ }
56
+
57
+ // ── 模拟 PaymentProvider ──
58
+ function mockProvider(name: string): PaymentProvider {
59
+ return {
60
+ name,
61
+ displayName: name,
62
+ supportedCurrencies: ["CNY"],
63
+ createPayment: vi.fn(),
64
+ verifyCallback: vi.fn(),
65
+ };
66
+ }
67
+
68
+ // ── 模拟 Factory ──
69
+ function mockFactory(name: string, priority: number, available: boolean): ProviderFactory {
70
+ return {
71
+ name,
72
+ priority,
73
+ isAvailable: () => available,
74
+ create: () => mockProvider(name),
75
+ };
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════
79
+ describe("ProviderRegistry", () => {
80
+ it("空的 factories → 空注册表", () => {
81
+ const reg = createProviderRegistry({}, []);
82
+ expect(reg.list()).toEqual([]);
83
+ expect(reg.selectOnline()).toBeNull();
84
+ });
85
+
86
+ it("单一可用 factory → 注册成功", () => {
87
+ const reg = createProviderRegistry({}, [mockFactory("alipay", 100, true)]);
88
+ expect(reg.list()).toEqual(["alipay"]);
89
+ expect(reg.get("alipay")).toBeDefined();
90
+ expect(reg.get("alipay")?.name).toBe("alipay");
91
+ });
92
+
93
+ it("不可用 factory → 不注册", () => {
94
+ const reg = createProviderRegistry({}, [mockFactory("alipay", 100, false)]);
95
+ expect(reg.list()).toEqual([]);
96
+ expect(reg.get("alipay")).toBeUndefined();
97
+ });
98
+
99
+ it("多个可用 factory → 按优先级选择", () => {
100
+ const reg = createProviderRegistry({}, [
101
+ mockFactory("wechat", 200, true),
102
+ mockFactory("alipay", 100, true),
103
+ ]);
104
+ const selected = reg.selectOnline();
105
+ expect(selected?.name).toBe("alipay"); // priority 100 < 200
106
+ });
107
+
108
+ it("高优先级不可用 → 选低优先级", () => {
109
+ const reg = createProviderRegistry({}, [
110
+ mockFactory("wechat", 100, false),
111
+ mockFactory("alipay", 200, true),
112
+ ]);
113
+ const selected = reg.selectOnline();
114
+ expect(selected?.name).toBe("alipay");
115
+ });
116
+
117
+ it("全部不可用 → selectOnline 返回 null", () => {
118
+ const reg = createProviderRegistry({}, [
119
+ mockFactory("wechat", 100, false),
120
+ mockFactory("alipay", 200, false),
121
+ ]);
122
+ expect(reg.selectOnline()).toBeNull();
123
+ });
124
+
125
+ it("get 不存在的 provider → undefined", () => {
126
+ const reg = createProviderRegistry({}, [mockFactory("alipay", 100, true)]);
127
+ expect(reg.get("wechat")).toBeUndefined();
128
+ });
129
+ });
130
+
131
+ // ═══════════════════════════════════════════════════════════════
132
+ describe("alipayFactory", () => {
133
+ it("env 有全部配置 → isAvailable = true", () => {
134
+ const env = { ALIPAY_APP_ID: "app", ALIPAY_PRIVATE_KEY: "key", ALIPAY_PUBLIC_KEY: "pub" };
135
+ expect(alipayFactory.isAvailable(env)).toBe(true);
136
+ });
137
+
138
+ it("env 缺少 ALIPAY_APP_ID → isAvailable = false", () => {
139
+ expect(alipayFactory.isAvailable({ ALIPAY_PRIVATE_KEY: "k", ALIPAY_PUBLIC_KEY: "p" })).toBe(false);
140
+ });
141
+
142
+ it("env 缺少 ALIPAY_PRIVATE_KEY → isAvailable = false", () => {
143
+ expect(alipayFactory.isAvailable({ ALIPAY_APP_ID: "a", ALIPAY_PUBLIC_KEY: "p" })).toBe(false);
144
+ });
145
+
146
+ it("env 缺少 ALIPAY_PUBLIC_KEY → isAvailable = false", () => {
147
+ expect(alipayFactory.isAvailable({ ALIPAY_APP_ID: "a", ALIPAY_PRIVATE_KEY: "k" })).toBe(false);
148
+ });
149
+
150
+ it("create 返回 AlipayProvider 实例", () => {
151
+ const env = { ALIPAY_APP_ID: "app123", ALIPAY_PRIVATE_KEY: "priv", ALIPAY_PUBLIC_KEY: "pub" };
152
+ const provider = alipayFactory.create(env);
153
+ expect(provider).toBeInstanceOf(AlipayProvider);
154
+ expect(provider.name).toBe("alipay");
155
+ expect(provider.supportedCurrencies).toContain("CNY");
156
+ });
157
+ });
158
+
159
+ // ═══════════════════════════════════════════════════════════════
160
+ describe("RSA2 签名", () => {
161
+ let privPem: string;
162
+ let pubPem: string;
163
+
164
+ beforeEach(async () => {
165
+ const keys = await generateTestKeys();
166
+ privPem = keys.privPem;
167
+ pubPem = keys.pubPem;
168
+ });
169
+
170
+ it("signRSA2 返回 Base64 字符串", async () => {
171
+ const sig = await signRSA2({ a: "1", b: "2" }, privPem);
172
+ expect(typeof sig).toBe("string");
173
+ expect(sig.length).toBeGreaterThan(0);
174
+ // Base64 格式
175
+ expect(sig).toMatch(/^[A-Za-z0-9+/]+=*$/);
176
+ });
177
+
178
+ it("sign + verify 往返验证", async () => {
179
+ const params = { method: "test", app_id: "123", amount: "100" };
180
+ const sig = await signRSA2(params, privPem);
181
+ const paramsWithSign = { ...params, sign: sig };
182
+ const valid = await verifyRSA2(paramsWithSign, pubPem);
183
+ expect(valid).toBe(true);
184
+ });
185
+
186
+ it("篡改参数后验证失败", async () => {
187
+ const params = { method: "test", app_id: "123", amount: "100" };
188
+ const sig = await signRSA2(params, privPem);
189
+ const tampered = { ...params, amount: "999", sign: sig };
190
+ const valid = await verifyRSA2(tampered, pubPem);
191
+ expect(valid).toBe(false);
192
+ });
193
+
194
+ it("无 sign 字段 → verifyRSA2 返回 false", async () => {
195
+ const valid = await verifyRSA2({ a: "1" }, pubPem);
196
+ expect(valid).toBe(false);
197
+ });
198
+
199
+ it("相同参数签名一致(确定性)", async () => {
200
+ const params = { x: "1", y: "2" };
201
+ const sig1 = await signRSA2(params, privPem);
202
+ const sig2 = await signRSA2(params, privPem);
203
+ expect(sig1).toBe(sig2);
204
+ });
205
+ });
206
+
207
+ // ═══════════════════════════════════════════════════════════════
208
+ describe("AlipayProvider", () => {
209
+ it("属性正确", () => {
210
+ const provider = new AlipayProvider({
211
+ appId: "test-app",
212
+ privateKey: "key",
213
+ alipayPublicKey: "pub",
214
+ });
215
+ expect(provider.name).toBe("alipay");
216
+ expect(provider.displayName).toBe("支付宝当面付");
217
+ expect(provider.supportedCurrencies).toEqual(["CNY"]);
218
+ });
219
+
220
+ it("verifyCallback 签名无效时抛错", async () => {
221
+ const provider = new AlipayProvider({
222
+ appId: "test-app",
223
+ privateKey: "key",
224
+ alipayPublicKey: TEST_PRIVATE_KEY, // 故意用错误 key
225
+ });
226
+ await expect(
227
+ provider.verifyCallback({ sign: "invalid", app_id: "test-app", trade_status: "TRADE_SUCCESS" }),
228
+ ).rejects.toThrow();
229
+ });
230
+ });
231
+
232
+ // ═══════════════════════════════════════════════════════════════
233
+ describe("stripeFactory", () => {
234
+ it("env 有全部配置 → isAvailable = true", () => {
235
+ const env = { STRIPE_SECRET_KEY: "sk_test", STRIPE_WEBHOOK_SECRET: "whsec_test" };
236
+ expect(stripeFactory.isAvailable(env)).toBe(true);
237
+ });
238
+
239
+ it("env 缺少 STRIPE_SECRET_KEY → isAvailable = false", () => {
240
+ expect(stripeFactory.isAvailable({ STRIPE_WEBHOOK_SECRET: "whsec" })).toBe(false);
241
+ });
242
+
243
+ it("env 缺少 STRIPE_WEBHOOK_SECRET → isAvailable = false", () => {
244
+ expect(stripeFactory.isAvailable({ STRIPE_SECRET_KEY: "sk" })).toBe(false);
245
+ });
246
+
247
+ it("create 返回 StripeProvider 实例", () => {
248
+ const env = { STRIPE_SECRET_KEY: "sk_test", STRIPE_WEBHOOK_SECRET: "whsec_test" };
249
+ const provider = stripeFactory.create(env);
250
+ expect(provider).toBeInstanceOf(StripeProvider);
251
+ expect(provider.name).toBe("stripe");
252
+ expect(provider.supportedCurrencies).toContain("USD");
253
+ });
254
+ });
255
+
256
+ // ═══════════════════════════════════════════════════════════════
257
+ describe("StripeProvider", () => {
258
+ it("属性正确", () => {
259
+ const provider = new StripeProvider("sk_test", "whsec_test");
260
+ expect(provider.name).toBe("stripe");
261
+ expect(provider.displayName).toBe("Stripe");
262
+ expect(provider.supportedCurrencies).toEqual(["USD", "EUR", "GBP", "CAD", "AUD", "JPY"]);
263
+ });
264
+
265
+ it("createPayment 构建正确的 URLSearchParams 并调用 Stripe API", async () => {
266
+ const provider = new StripeProvider("sk_test", "whsec_test");
267
+ const mockFetch = vi.fn().mockResolvedValue({
268
+ ok: true,
269
+ json: async () => ({ id: "cs_test_123", url: "https://checkout.stripe.com/c/test_123" }),
270
+ });
271
+ globalThis.fetch = mockFetch as unknown as typeof fetch;
272
+
273
+ const result = await provider.createPayment({
274
+ orderNo: "ORD001",
275
+ amountCents: 2999,
276
+ currency: "USD",
277
+ notifyUrl: "https://example.com/pay/callback",
278
+ });
279
+
280
+ expect(result.redirectUrl).toBe("https://checkout.stripe.com/c/test_123");
281
+ expect(result.providerTradeNo).toBe("cs_test_123");
282
+
283
+ // 验证 fetch 调用参数
284
+ expect(mockFetch).toHaveBeenCalledTimes(1);
285
+ const callArgs = mockFetch.mock.calls[0];
286
+ expect(callArgs[0]).toBe("https://api.stripe.com/v1/checkout/sessions");
287
+ expect(callArgs[1].method).toBe("POST");
288
+ expect(callArgs[1].headers["Authorization"]).toBe("Bearer sk_test");
289
+ expect(callArgs[1].body).toContain("ORD001");
290
+ });
291
+
292
+ it("createPayment API 错误时抛错", async () => {
293
+ const provider = new StripeProvider("sk_test", "whsec_test");
294
+ globalThis.fetch = vi.fn().mockResolvedValue({
295
+ ok: false,
296
+ status: 401,
297
+ text: async () => "Unauthorized",
298
+ }) as unknown as typeof fetch;
299
+
300
+ await expect(
301
+ provider.createPayment({
302
+ orderNo: "ORD001",
303
+ amountCents: 2999,
304
+ currency: "USD",
305
+ notifyUrl: "https://example.com/pay/callback",
306
+ }),
307
+ ).rejects.toThrow("Stripe API error: 401");
308
+ });
309
+
310
+ it("createPayment 无返回 URL 时抛错", async () => {
311
+ const provider = new StripeProvider("sk_test", "whsec_test");
312
+ globalThis.fetch = vi.fn().mockResolvedValue({
313
+ ok: true,
314
+ json: async () => ({ id: "cs_test_123" }), // 无 url 字段
315
+ }) as unknown as typeof fetch;
316
+
317
+ await expect(
318
+ provider.createPayment({
319
+ orderNo: "ORD001",
320
+ amountCents: 2999,
321
+ currency: "USD",
322
+ notifyUrl: "https://example.com/pay/callback",
323
+ }),
324
+ ).rejects.toThrow("Stripe returned no checkout URL");
325
+ });
326
+
327
+ it("verifyCallback 验证签名并通过", async () => {
328
+ const provider = new StripeProvider("sk_test", "whsec_test");
329
+ const validPayload = JSON.stringify({
330
+ type: "checkout.session.completed",
331
+ data: {
332
+ object: {
333
+ metadata: { order_no: "ORD001" },
334
+ amount_total: 2999,
335
+ currency: "usd",
336
+ payment_intent: "pi_test_123",
337
+ id: "cs_test_123",
338
+ },
339
+ },
340
+ });
341
+
342
+ // 构建合法的 Stripe-Signature: t=<timestamp>,v1=<hmac>
343
+ const webhookSecret = "whsec_test";
344
+ const timestamp = Math.floor(Date.now() / 1000);
345
+ const encoder = new TextEncoder();
346
+ const signedPayload = `${timestamp}.${validPayload}`;
347
+ const key = await crypto.subtle.importKey("raw", encoder.encode(webhookSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
348
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload));
349
+ const hexSig = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
350
+
351
+ const result = await provider.verifyCallback({
352
+ _raw_body: validPayload,
353
+ _stripe_signature: `t=${timestamp},v1=${hexSig}`,
354
+ });
355
+
356
+ expect(result.orderNo).toBe("ORD001");
357
+ expect(result.amountCents).toBe(2999);
358
+ expect(result.currency).toBe("USD");
359
+ expect(result.providerTradeNo).toBe("pi_test_123");
360
+ });
361
+
362
+ it("verifyCallback 签名无效时抛错", async () => {
363
+ const provider = new StripeProvider("sk_test", "whsec_test");
364
+ await expect(
365
+ provider.verifyCallback({
366
+ _raw_body: JSON.stringify({ type: "checkout.session.completed", data: { object: {} } }),
367
+ _stripe_signature: "t=1234567890,v1=invalid",
368
+ }),
369
+ ).rejects.toThrow("Stripe webhook signature invalid");
370
+ });
371
+
372
+ it("verifyCallback 缺少签名头时抛错", async () => {
373
+ const provider = new StripeProvider("sk_test", "whsec_test");
374
+ await expect(
375
+ provider.verifyCallback({}),
376
+ ).rejects.toThrow("Stripe callback missing signature headers");
377
+ });
378
+
379
+ it("verifyCallback 事件类型错误时抛错", async () => {
380
+ const provider = new StripeProvider("sk_test", "whsec_test");
381
+ const payload = JSON.stringify({ type: "payment_intent.succeeded", data: { object: {} } });
382
+ const timestamp = Math.floor(Date.now() / 1000);
383
+ const encoder = new TextEncoder();
384
+ const signedPayload = `${timestamp}.${payload}`;
385
+ const key = await crypto.subtle.importKey("raw", encoder.encode("whsec_test"), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
386
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(signedPayload));
387
+ const hexSig = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
388
+
389
+ await expect(
390
+ provider.verifyCallback({ _raw_body: payload, _stripe_signature: `t=${timestamp},v1=${hexSig}` }),
391
+ ).rejects.toThrow("Unexpected Stripe event type");
392
+ });
393
+ });
394
+
395
+ // ═══════════════════════════════════════════════════════════════
396
+ describe("trc20Factory", () => {
397
+ it("env 有全部配置 → isAvailable = true", () => {
398
+ const env = { TRC20_WALLET_ADDRESS: "TXYZ123", TRONGRID_API_KEY: "key123" };
399
+ expect(trc20Factory.isAvailable(env)).toBe(true);
400
+ });
401
+
402
+ it("env 缺少 TRC20_WALLET_ADDRESS → isAvailable = false", () => {
403
+ expect(trc20Factory.isAvailable({ TRONGRID_API_KEY: "key" })).toBe(false);
404
+ });
405
+
406
+ it("env 缺少 TRONGRID_API_KEY → isAvailable = false", () => {
407
+ expect(trc20Factory.isAvailable({ TRC20_WALLET_ADDRESS: "addr" })).toBe(false);
408
+ });
409
+
410
+ it("create 返回 Trc20Provider 实例", () => {
411
+ const env = { TRC20_WALLET_ADDRESS: "TXYZ123", TRONGRID_API_KEY: "key123" };
412
+ const provider = trc20Factory.create(env);
413
+ expect(provider).toBeInstanceOf(Trc20Provider);
414
+ expect(provider.name).toBe("usdt_trc20");
415
+ expect(provider.supportedCurrencies).toContain("USDT");
416
+ });
417
+ });
418
+
419
+ // ═══════════════════════════════════════════════════════════════
420
+ describe("Trc20Provider", () => {
421
+ it("属性正确", () => {
422
+ const provider = new Trc20Provider("TXYZ123", "key123");
423
+ expect(provider.name).toBe("usdt_trc20");
424
+ expect(provider.displayName).toBe("USDT (TRC20)");
425
+ expect(provider.supportedCurrencies).toEqual(["USDT"]);
426
+ });
427
+
428
+ it("createPayment 返回地址、金额和 Memo", async () => {
429
+ const provider = new Trc20Provider("TXYZ123", "key123");
430
+ const result = await provider.createPayment({
431
+ orderNo: "P1A2B3C4",
432
+ amountCents: 5000, // $50.00
433
+ currency: "USDT",
434
+ notifyUrl: "https://example.com/pay/callback",
435
+ });
436
+
437
+ expect(result.raw).toBeDefined();
438
+ expect(result.raw!.address).toBe("TXYZ123");
439
+ expect(result.raw!.amount).toBe("50.000000");
440
+ // P1A2B3C4 中的数字是 1,2,3,4 → "1234" → padStart 8 → "00001234"
441
+ expect(result.raw!.memo).toBe("00001234");
442
+ expect(result.raw!.network).toBe("TRC20");
443
+ expect(result.raw!.warnings).toHaveLength(3);
444
+ });
445
+
446
+ it("createPayment 从订单号提取数字 Memo", async () => {
447
+ const provider = new Trc20Provider("TXYZ123", "key123");
448
+ // ORD-00001234 → 数字部分 00001234 → 最后8位 "00001234"
449
+ const result = await provider.createPayment({
450
+ orderNo: "ORD-00001234",
451
+ amountCents: 1000,
452
+ currency: "USDT",
453
+ notifyUrl: "https://example.com/pay/callback",
454
+ });
455
+ expect(result.raw!.memo).toBe("00001234");
456
+ });
457
+
458
+ it("verifyCallback 始终抛错", async () => {
459
+ const provider = new Trc20Provider("TXYZ123", "key123");
460
+ await expect(provider.verifyCallback({})).rejects.toThrow("USDT_TRC20 does not support HTTP callbacks");
461
+ });
462
+
463
+ it("queryStatus 链上无交易时返回 paid=false", async () => {
464
+ const provider = new Trc20Provider("TXYZ123", "key123");
465
+ globalThis.fetch = vi.fn().mockResolvedValue({
466
+ ok: true,
467
+ json: async () => ({ data: [], total: 0 }),
468
+ }) as unknown as typeof fetch;
469
+
470
+ const result = await provider.queryStatus("ORD-00001234");
471
+ expect(result.paid).toBe(false);
472
+ });
473
+
474
+ it("queryStatus 链上有匹配交易时返回 paid=true", async () => {
475
+ const provider = new Trc20Provider("TXYZ123", "key123");
476
+ globalThis.fetch = vi.fn().mockResolvedValue({
477
+ ok: true,
478
+ json: async () => ({
479
+ data: [
480
+ {
481
+ transaction_id: "txn_abc123",
482
+ value: "50000000", // 50 USDT (6 decimals)
483
+ token_info: { symbol: "USDT", decimals: 6 },
484
+ block_timestamp: Date.now(),
485
+ },
486
+ ],
487
+ total: 1,
488
+ }),
489
+ }) as unknown as typeof fetch;
490
+
491
+ const result = await provider.queryStatus("ORD-00001234");
492
+ expect(result.paid).toBe(true);
493
+ expect(result.providerTradeNo).toBe("txn_abc123");
494
+ });
495
+
496
+ it("queryStatus API 错误时返回 paid=false", async () => {
497
+ const provider = new Trc20Provider("TXYZ123", "key123");
498
+ globalThis.fetch = vi.fn().mockResolvedValue({
499
+ ok: false,
500
+ status: 429, // rate limit
501
+ }) as unknown as typeof fetch;
502
+
503
+ const result = await provider.queryStatus("ORD-00001234");
504
+ expect(result.paid).toBe(false);
505
+ });
506
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 支付功能模块 — 类型定义
3
+ *
4
+ * 纯接口文件,零运行时依赖。
5
+ * 所有支付 Provider 实现都必须实现 PaymentProvider 接口。
6
+ */
7
+
8
+ // ═══════════════════════════════════════════════════════════════════════════════
9
+ // 支付操作数据类型
10
+ // ═══════════════════════════════════════════════════════════════════════════════
11
+
12
+ export interface CreatePaymentInput {
13
+ orderNo: string;
14
+ amountCents: number;
15
+ currency: string;
16
+ notifyUrl: string;
17
+ returnUrl?: string;
18
+ description?: string;
19
+ metadata?: Record<string, string>;
20
+ }
21
+
22
+ export interface CreatePaymentResult {
23
+ providerTradeNo?: string;
24
+ qrCode?: string;
25
+ redirectUrl?: string;
26
+ raw?: Record<string, unknown>;
27
+ }
28
+
29
+ export interface CallbackResult {
30
+ orderNo: string;
31
+ providerTradeNo: string;
32
+ amountCents: number;
33
+ currency: string;
34
+ paidAt: string;
35
+ raw?: Record<string, unknown>;
36
+ }
37
+
38
+ export interface QueryStatusResult {
39
+ paid: boolean;
40
+ providerTradeNo?: string;
41
+ }
42
+
43
+ export interface RefundInput {
44
+ providerTradeNo: string;
45
+ refundCents: number;
46
+ reason?: string;
47
+ refundNo?: string;
48
+ }
49
+
50
+ export interface RefundResult {
51
+ success: boolean;
52
+ providerRefundNo?: string;
53
+ status: string;
54
+ }
55
+
56
+ // ═══════════════════════════════════════════════════════════════════════════════
57
+ // PaymentProvider 接口
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+
60
+ export interface PaymentProvider {
61
+ readonly name: string;
62
+ readonly displayName: string;
63
+ readonly supportedCurrencies: string[];
64
+ createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult>;
65
+ verifyCallback(params: Record<string, string>): Promise<CallbackResult>;
66
+ queryStatus?(tradeNo: string): Promise<QueryStatusResult>;
67
+ refund?(input: RefundInput): Promise<RefundResult>;
68
+ }
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════════
71
+ // Provider 注册表类型
72
+ // ═══════════════════════════════════════════════════════════════════════════════
73
+
74
+ export interface ProviderRegistry {
75
+ get(name: string): PaymentProvider | undefined;
76
+ selectOnline(): PaymentProvider | null;
77
+ list(): string[];
78
+ }
79
+
80
+ export interface ProviderFactory {
81
+ name: string;
82
+ priority: number;
83
+ isAvailable(env: Record<string, unknown>): boolean;
84
+ create(env: Record<string, unknown>): PaymentProvider;
85
+
86
+ /**
87
+ * 从数据库解密后的配置创建 Provider。
88
+ * 可选方法——未实现时,注册表会将 config 合入 env 再调用 create()。
89
+ *
90
+ * @param config - 解密后的扁平配置对象(键名与 env var 一致,如 { ZPAY_PID: "xxx" })
91
+ */
92
+ fromDbConfig?(config: Record<string, unknown>): PaymentProvider;
93
+ }