@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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* email-resend — Resend 邮件发送插件
|
|
3
|
+
*
|
|
4
|
+
* 通过 Resend API 发送事务邮件,支持:
|
|
5
|
+
* - 模板插值({{变量}})+ HTML 转义
|
|
6
|
+
* - 3 次重试(指数退避)
|
|
7
|
+
* - 邮件日志记录(可选,需传入 Drizzle DB 实例)
|
|
8
|
+
*
|
|
9
|
+
* 来源:eshop src/services/email-service.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// 模板引擎
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
export function escapeHtml(s: string): string {
|
|
17
|
+
return s.replace(/[&<>"']/g, (c) =>
|
|
18
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] ?? c),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function interpolate(template: string, data: Record<string, string>): string {
|
|
23
|
+
return template
|
|
24
|
+
.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(String(data[key] ?? "")))
|
|
25
|
+
.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) =>
|
|
26
|
+
data[key] ? content : "",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// EmailService
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
export interface EmailTemplate {
|
|
35
|
+
subject: string;
|
|
36
|
+
html: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResendConfig {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
from?: string;
|
|
42
|
+
defaultFrom?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SendEmailOptions {
|
|
46
|
+
to: string;
|
|
47
|
+
subject: string;
|
|
48
|
+
html: string;
|
|
49
|
+
from?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SendResult {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
messageId?: string;
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resend 邮件发送服务
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { EmailService } from "@usethink/cf-core/plugins/email-resend";
|
|
64
|
+
*
|
|
65
|
+
* const email = new EmailService({ apiKey: env.RESEND_API_KEY, from: "noreply@example.com" });
|
|
66
|
+
* const result = await email.send({
|
|
67
|
+
* to: "buyer@example.com",
|
|
68
|
+
* subject: "订单确认",
|
|
69
|
+
* html: "<h1>您的订单已确认</h1>",
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export class EmailService {
|
|
74
|
+
readonly name = "email-resend";
|
|
75
|
+
readonly version = "0.1.0";
|
|
76
|
+
|
|
77
|
+
private config: ResendConfig;
|
|
78
|
+
|
|
79
|
+
constructor(config: ResendConfig) {
|
|
80
|
+
this.config = config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 发送邮件
|
|
85
|
+
*/
|
|
86
|
+
async send(opts: SendEmailOptions): Promise<SendResult> {
|
|
87
|
+
const { to, subject, html } = opts;
|
|
88
|
+
const from = opts.from || this.config.from || this.config.defaultFrom || "noreply@example.com";
|
|
89
|
+
|
|
90
|
+
if (!to || !to.includes("@")) {
|
|
91
|
+
return { ok: false, error: "无效的收件人邮箱" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const MAX_RETRIES = 3;
|
|
95
|
+
const RETRY_DELAYS = [500, 1000, 2000];
|
|
96
|
+
let lastError = "";
|
|
97
|
+
|
|
98
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch("https://api.resend.com/emails", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({ from, to, subject, html }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const data = (await res.json()) as { id?: string; message?: string };
|
|
110
|
+
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
return { ok: true, messageId: data.id };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (res.status >= 400 && res.status < 500) {
|
|
116
|
+
return { ok: false, error: data.message || `HTTP ${res.status}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lastError = data.message || `HTTP ${res.status}`;
|
|
120
|
+
if (attempt < MAX_RETRIES) {
|
|
121
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt - 1]));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ok: false, error: lastError };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
128
|
+
if (attempt < MAX_RETRIES) {
|
|
129
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt - 1]));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
return { ok: false, error: lastError };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { ok: false, error: lastError };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 使用模板发送邮件
|
|
141
|
+
*/
|
|
142
|
+
async sendWithTemplate(
|
|
143
|
+
to: string,
|
|
144
|
+
template: EmailTemplate,
|
|
145
|
+
data: Record<string, string>,
|
|
146
|
+
opts?: { from?: string },
|
|
147
|
+
): Promise<SendResult> {
|
|
148
|
+
const subject = interpolate(template.subject, data);
|
|
149
|
+
const html = interpolate(template.html, data);
|
|
150
|
+
return this.send({ to, subject, html, from: opts?.from });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 健康检查 — 验证 API Key 是否有效
|
|
155
|
+
*/
|
|
156
|
+
async healthCheck(): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch("https://api.resend.com/emails", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({ from: "test@test.com", to: "test@test.com", subject: "", html: "" }),
|
|
165
|
+
});
|
|
166
|
+
// 422 = 参数无效但 Key 有效; 401/403 = Key 无效
|
|
167
|
+
return res.status !== 401 && res.status !== 403;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { escapeHtml, interpolate } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("escapeHtml", () => {
|
|
5
|
+
it("转义特殊字符", () => {
|
|
6
|
+
expect(escapeHtml("<script>alert('xss')</script>")).toBe(
|
|
7
|
+
"<script>alert('xss')</script>",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("普通文本不变", () => {
|
|
12
|
+
expect(escapeHtml("hello world")).toBe("hello world");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("转义 & 和引号", () => {
|
|
16
|
+
expect(escapeHtml('a & b "c"')).toBe("a & b "c"");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("interpolate", () => {
|
|
21
|
+
it("替换变量", () => {
|
|
22
|
+
const result = interpolate("Hello {{name}}, your order is {{orderNo}}", {
|
|
23
|
+
name: "Alice",
|
|
24
|
+
orderNo: "12345",
|
|
25
|
+
});
|
|
26
|
+
expect(result).toBe("Hello Alice, your order is 12345");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("缺失变量替换为空", () => {
|
|
30
|
+
const result = interpolate("Hello {{name}}", {});
|
|
31
|
+
expect(result).toBe("Hello ");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("变量值自动 HTML 转义", () => {
|
|
35
|
+
const result = interpolate("{{content}}", { content: "<b>bold</b>" });
|
|
36
|
+
expect(result).toBe("<b>bold</b>");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("条件块", () => {
|
|
40
|
+
const template = "{{#if showNote}}Note: {{note}}{{/if}}";
|
|
41
|
+
expect(interpolate(template, { showNote: "yes", note: "hello" })).toBe("Note: hello");
|
|
42
|
+
expect(interpolate(template, { note: "hello" })).toBe("");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支付功能模块 — HTTP 工具
|
|
3
|
+
*
|
|
4
|
+
* 带超时和指数退避重试的 fetch 封装。
|
|
5
|
+
* 所有第三方 API 调用都应通过此函数,避免 Workers CPU 耗尽。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
9
|
+
const FETCH_RETRIES = 2;
|
|
10
|
+
const RETRY_DELAYS = [500, 1500];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 带超时和指数退避重试的 fetch 封装。
|
|
14
|
+
*
|
|
15
|
+
* HTTP 4xx 不重试(客户端错误),5xx/网络错误重试。
|
|
16
|
+
* 超时通过 AbortController 实现,兼容 Workers 环境。
|
|
17
|
+
*/
|
|
18
|
+
export async function fetchWithRetry(
|
|
19
|
+
url: string,
|
|
20
|
+
options: RequestInit & { retries?: number; timeoutMs?: number } = {},
|
|
21
|
+
): Promise<Response> {
|
|
22
|
+
const maxRetries = options.retries ?? FETCH_RETRIES;
|
|
23
|
+
const timeoutMs = options.timeoutMs ?? FETCH_TIMEOUT_MS;
|
|
24
|
+
let lastError = "";
|
|
25
|
+
|
|
26
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
30
|
+
|
|
31
|
+
const resp = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
|
|
38
|
+
// 4xx 不重试(客户端错误)
|
|
39
|
+
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) return resp;
|
|
40
|
+
|
|
41
|
+
// 5xx 或 429 重试
|
|
42
|
+
if (!resp.ok && attempt <= maxRetries) {
|
|
43
|
+
lastError = `HTTP ${resp.status}`;
|
|
44
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt - 1] ?? 1000));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return resp;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
51
|
+
lastError = `Timeout after ${timeoutMs}ms`;
|
|
52
|
+
} else {
|
|
53
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (attempt <= maxRetries) {
|
|
57
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt - 1] ?? 1000));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`[fetchWithRetry] ${lastError} (after ${attempt - 1} retries)`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error(`[fetchWithRetry] ${lastError} (after ${maxRetries + 1} attempts)`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @usethink/cf-core/features/payment — 支付功能模块
|
|
3
|
+
*
|
|
4
|
+
* 统一导出所有支付相关能力。支持两种导入方式:
|
|
5
|
+
*
|
|
6
|
+
* 1. 从根导入(适合小项目):
|
|
7
|
+
* import { alipayFactory, stripeFactory } from "@usethink/cf-core/features/payment";
|
|
8
|
+
*
|
|
9
|
+
* 2. 按子路径导入(推荐,tree-shakeable):
|
|
10
|
+
* import { alipayFactory } from "@usethink/cf-core/features/payment/providers/alipay";
|
|
11
|
+
* import { stripeFactory } from "@usethink/cf-core/features/payment/providers/stripe";
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── 类型 ──
|
|
15
|
+
export type {
|
|
16
|
+
CreatePaymentInput,
|
|
17
|
+
CreatePaymentResult,
|
|
18
|
+
CallbackResult,
|
|
19
|
+
QueryStatusResult,
|
|
20
|
+
RefundInput,
|
|
21
|
+
RefundResult,
|
|
22
|
+
PaymentProvider,
|
|
23
|
+
ProviderRegistry,
|
|
24
|
+
ProviderFactory,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
// ── 注册表 ──
|
|
28
|
+
export { createProviderRegistry } from "./registry";
|
|
29
|
+
export type { DbProviderConfig, DbProviderConfigMap } from "./registry";
|
|
30
|
+
|
|
31
|
+
// ── 支付宝 ──
|
|
32
|
+
export { AlipayProvider, alipayFactory, signRSA2, verifyRSA2 } from "./providers/alipay";
|
|
33
|
+
export type { AlipayConfig } from "./providers/alipay";
|
|
34
|
+
|
|
35
|
+
// ── Stripe ──
|
|
36
|
+
export { StripeProvider, stripeFactory } from "./providers/stripe";
|
|
37
|
+
|
|
38
|
+
// ── USDT/TRC20 ──
|
|
39
|
+
export { Trc20Provider, trc20Factory } from "./providers/trc20";
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支付功能模块 — 支付宝当面付 Provider
|
|
3
|
+
*
|
|
4
|
+
* 包含:
|
|
5
|
+
* - RSA2 签名工具(Web Crypto API,零依赖)
|
|
6
|
+
* - AlipayProvider 类(当面付:创建二维码、回调验签、查询状态)
|
|
7
|
+
* - alipayFactory 工厂
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CreatePaymentInput,
|
|
12
|
+
CreatePaymentResult,
|
|
13
|
+
CallbackResult,
|
|
14
|
+
QueryStatusResult,
|
|
15
|
+
PaymentProvider,
|
|
16
|
+
ProviderFactory,
|
|
17
|
+
} from "../types";
|
|
18
|
+
import { fetchWithRetry } from "../fetch-utils";
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// RSA2 签名工具(Web Crypto API,零依赖)
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
|
|
26
|
+
function buildSignString(params: Record<string, string>): string {
|
|
27
|
+
return Object.keys(params)
|
|
28
|
+
.filter((k) => k !== "sign" && k !== "sign_type" && params[k] !== "" && params[k] !== undefined)
|
|
29
|
+
.sort()
|
|
30
|
+
.map((k) => `${k}=${params[k]}`)
|
|
31
|
+
.join("&");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
35
|
+
const s = atob(base64);
|
|
36
|
+
const bytes = new Uint8Array(s.length);
|
|
37
|
+
for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
42
|
+
let binary = "";
|
|
43
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
44
|
+
return btoa(binary);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function importPrivateKey(pem: string): Promise<CryptoKey> {
|
|
48
|
+
const body = pem.replace(/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/g, "").replace(/-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/g, "").replace(/\s+/g, "");
|
|
49
|
+
const der = base64ToUint8Array(body);
|
|
50
|
+
try {
|
|
51
|
+
return await crypto.subtle.importKey("pkcs8", der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
52
|
+
} catch {
|
|
53
|
+
const header = [0x30,0x82,0x01,0x22,0x30,0x0d,0x06,0x09,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x01,0x05,0x00,0x04,0x82,0x01,0x0f];
|
|
54
|
+
const pkcs8 = new Uint8Array(header.length + der.length);
|
|
55
|
+
pkcs8.set(header); pkcs8.set(der, header.length);
|
|
56
|
+
return await crypto.subtle.importKey("pkcs8", pkcs8, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function importPublicKey(pem: string): Promise<CryptoKey> {
|
|
61
|
+
const body = pem.replace(/-----BEGIN\s+(RSA\s+)?PUBLIC\s+KEY-----/g, "").replace(/-----END\s+(RSA\s+)?PUBLIC\s+KEY-----/g, "").replace(/\s+/g, "");
|
|
62
|
+
return await crypto.subtle.importKey("spki", base64ToUint8Array(body), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** RSA2 签名(SHA256withRSA) */
|
|
66
|
+
export async function signRSA2(params: Record<string, string>, privateKeyPem: string): Promise<string> {
|
|
67
|
+
const key = await importPrivateKey(privateKeyPem);
|
|
68
|
+
const sig = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, encoder.encode(buildSignString(params)));
|
|
69
|
+
return uint8ArrayToBase64(new Uint8Array(sig));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** RSA2 验签 */
|
|
73
|
+
export async function verifyRSA2(params: Record<string, string>, publicKeyPem: string): Promise<boolean> {
|
|
74
|
+
const sign = params["sign"];
|
|
75
|
+
if (!sign) return false;
|
|
76
|
+
const key = await importPublicKey(publicKeyPem);
|
|
77
|
+
return crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, base64ToUint8Array(sign), encoder.encode(buildSignString(params)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
81
|
+
// AlipayProvider — 支付宝当面付
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
export interface AlipayConfig {
|
|
85
|
+
appId: string;
|
|
86
|
+
privateKey: string;
|
|
87
|
+
alipayPublicKey: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class AlipayProvider implements PaymentProvider {
|
|
91
|
+
readonly name = "alipay";
|
|
92
|
+
readonly displayName = "支付宝当面付";
|
|
93
|
+
readonly supportedCurrencies = ["CNY"];
|
|
94
|
+
|
|
95
|
+
constructor(private readonly config: AlipayConfig) {}
|
|
96
|
+
|
|
97
|
+
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult> {
|
|
98
|
+
const bizContent = JSON.stringify({
|
|
99
|
+
out_trade_no: input.orderNo,
|
|
100
|
+
total_amount: (input.amountCents / 100).toFixed(2),
|
|
101
|
+
subject: input.metadata?.subject || input.description || "商品购买",
|
|
102
|
+
});
|
|
103
|
+
const timestamp = new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
104
|
+
const params: Record<string, string> = {
|
|
105
|
+
app_id: this.config.appId, method: "alipay.trade.precreate", charset: "utf-8",
|
|
106
|
+
sign_type: "RSA2", timestamp, version: "1.0", notify_url: input.notifyUrl, biz_content: bizContent,
|
|
107
|
+
};
|
|
108
|
+
params.sign = await signRSA2(params, this.config.privateKey);
|
|
109
|
+
|
|
110
|
+
const resp = await fetchWithRetry("https://openapi.alipay.com/gateway.do", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
|
|
113
|
+
body: new URLSearchParams(params).toString(),
|
|
114
|
+
timeoutMs: 10_000,
|
|
115
|
+
retries: 2,
|
|
116
|
+
});
|
|
117
|
+
if (!resp.ok) throw new Error(`Alipay API HTTP error: ${resp.status}`);
|
|
118
|
+
|
|
119
|
+
const data = await resp.json() as { alipay_trade_precreate_response?: { code?: string; msg?: string; sub_msg?: string; qr_code?: string } };
|
|
120
|
+
const result = data.alipay_trade_precreate_response;
|
|
121
|
+
if (!result || result.code !== "10000") throw new Error(`Alipay precreate failed: ${result?.sub_msg || result?.msg || "unknown"}`);
|
|
122
|
+
return { qrCode: result.qr_code || "" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async verifyCallback(params: Record<string, string>): Promise<CallbackResult> {
|
|
126
|
+
if (!(await verifyRSA2(params, this.config.alipayPublicKey))) throw new Error("Alipay callback signature invalid");
|
|
127
|
+
if (params.app_id !== this.config.appId) throw new Error("Alipay callback app_id mismatch");
|
|
128
|
+
if (params.trade_status !== "TRADE_SUCCESS") throw new Error(`Unexpected trade_status: ${params.trade_status}`);
|
|
129
|
+
return {
|
|
130
|
+
orderNo: params.out_trade_no, providerTradeNo: params.trade_no,
|
|
131
|
+
amountCents: Math.round(parseFloat(params.total_amount || "0") * 100),
|
|
132
|
+
currency: "CNY", paidAt: params.gmt_payment || new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async queryStatus(orderNo: string): Promise<QueryStatusResult> {
|
|
137
|
+
const timestamp = new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
138
|
+
const params: Record<string, string> = {
|
|
139
|
+
app_id: this.config.appId, method: "alipay.trade.query", charset: "utf-8",
|
|
140
|
+
sign_type: "RSA2", timestamp, version: "1.0", biz_content: JSON.stringify({ out_trade_no: orderNo }),
|
|
141
|
+
};
|
|
142
|
+
params.sign = await signRSA2(params, this.config.privateKey);
|
|
143
|
+
const resp = await fetchWithRetry("https://openapi.alipay.com/gateway.do", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" },
|
|
146
|
+
body: new URLSearchParams(params).toString(),
|
|
147
|
+
timeoutMs: 10_000,
|
|
148
|
+
retries: 2,
|
|
149
|
+
});
|
|
150
|
+
if (!resp.ok) throw new Error(`Alipay API HTTP error: ${resp.status}`);
|
|
151
|
+
const data = await resp.json() as { alipay_trade_query_response?: { trade_status?: string } };
|
|
152
|
+
return { paid: data.alipay_trade_query_response?.trade_status === "TRADE_SUCCESS" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
// 支付宝工厂
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
|
|
160
|
+
export const alipayFactory: ProviderFactory = {
|
|
161
|
+
name: "alipay",
|
|
162
|
+
priority: 100,
|
|
163
|
+
isAvailable(env) { return !!(env.ALIPAY_APP_ID && env.ALIPAY_PRIVATE_KEY && env.ALIPAY_PUBLIC_KEY); },
|
|
164
|
+
create(env) {
|
|
165
|
+
return new AlipayProvider({
|
|
166
|
+
appId: env.ALIPAY_APP_ID as string,
|
|
167
|
+
privateKey: env.ALIPAY_PRIVATE_KEY as string,
|
|
168
|
+
alipayPublicKey: env.ALIPAY_PUBLIC_KEY as string,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支付功能模块 — Stripe Provider
|
|
3
|
+
*
|
|
4
|
+
* 包含:
|
|
5
|
+
* - Stripe Checkout Sessions(单次支付)
|
|
6
|
+
* - Webhook 签名验证(HMAC-SHA256)
|
|
7
|
+
* - stripeFactory 工厂
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CreatePaymentInput,
|
|
12
|
+
CreatePaymentResult,
|
|
13
|
+
CallbackResult,
|
|
14
|
+
QueryStatusResult,
|
|
15
|
+
PaymentProvider,
|
|
16
|
+
ProviderFactory,
|
|
17
|
+
} from "../types";
|
|
18
|
+
import { fetchWithRetry } from "../fetch-utils";
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// Stripe API 常量
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
const STRIPE_API_BASE = "https://api.stripe.com/v1";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 验证 Stripe webhook 签名(HMAC-SHA256)。
|
|
28
|
+
*
|
|
29
|
+
* Stripe webhook 签名机制:
|
|
30
|
+
* - 每个 webhook 请求携带 Stripe-Signature 头
|
|
31
|
+
* - 格式: t=timestamp,v1=signature
|
|
32
|
+
* - 签名 = HMAC-SHA256(webhookSecret, timestamp.rawBody)
|
|
33
|
+
*
|
|
34
|
+
* 参考 https://docs.stripe.com/webhooks/signatures
|
|
35
|
+
*/
|
|
36
|
+
async function verifyStripeSignature(
|
|
37
|
+
payload: string,
|
|
38
|
+
sigHeader: string,
|
|
39
|
+
webhookSecret: string,
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
const parts = sigHeader.split(",");
|
|
42
|
+
let timestamp = "";
|
|
43
|
+
let signature = "";
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
const [key, value] = part.split("=");
|
|
46
|
+
if (key === "t") timestamp = value;
|
|
47
|
+
if (key === "v1") signature = value;
|
|
48
|
+
}
|
|
49
|
+
if (!timestamp || !signature) return false;
|
|
50
|
+
|
|
51
|
+
// 时间戳偏差校验(±5 分钟,防止重放攻击)
|
|
52
|
+
const sigTime = parseInt(timestamp, 10) * 1000;
|
|
53
|
+
if (isNaN(sigTime) || Math.abs(Date.now() - sigTime) > 5 * 60 * 1000) return false;
|
|
54
|
+
|
|
55
|
+
// HMAC-SHA256(webhookSecret, timestamp.payload)
|
|
56
|
+
const encoder = new TextEncoder();
|
|
57
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
58
|
+
const key = await crypto.subtle.importKey(
|
|
59
|
+
"raw",
|
|
60
|
+
encoder.encode(webhookSecret),
|
|
61
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
62
|
+
false,
|
|
63
|
+
["verify"],
|
|
64
|
+
);
|
|
65
|
+
const signatureBytes = new Uint8Array(
|
|
66
|
+
signature.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [],
|
|
67
|
+
);
|
|
68
|
+
return crypto.subtle.verify("HMAC", key, signatureBytes, encoder.encode(signedPayload));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// StripeProvider — 国际信用卡支付
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
export class StripeProvider implements PaymentProvider {
|
|
76
|
+
readonly name = "stripe";
|
|
77
|
+
readonly displayName = "Stripe";
|
|
78
|
+
readonly supportedCurrencies = ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"];
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
private readonly secretKey: string,
|
|
82
|
+
private readonly webhookSecret: string,
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 创建 Stripe Checkout Session。
|
|
87
|
+
*
|
|
88
|
+
* 使用 URLSearchParams(非 stripe-node SDK)构建请求,
|
|
89
|
+
* 兼容 Workers 环境(无 Node.js 内置模块)。
|
|
90
|
+
*/
|
|
91
|
+
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentResult> {
|
|
92
|
+
const origin = (() => {
|
|
93
|
+
try { return new URL(input.notifyUrl).origin; } catch { return "https://localhost"; }
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
const params = new URLSearchParams({
|
|
97
|
+
mode: "payment",
|
|
98
|
+
"line_items[0][price_data][currency]": input.currency.toLowerCase(),
|
|
99
|
+
"line_items[0][price_data][product_data][name]": input.metadata?.subject || "商品购买",
|
|
100
|
+
"line_items[0][price_data][unit_amount]": String(input.amountCents),
|
|
101
|
+
"line_items[0][quantity]": "1",
|
|
102
|
+
"metadata[order_no]": input.orderNo,
|
|
103
|
+
success_url: `${origin}/lookup?session_id={CHECKOUT_SESSION_ID}`,
|
|
104
|
+
cancel_url: `${origin}/shop`,
|
|
105
|
+
});
|
|
106
|
+
if (input.returnUrl) params.set("success_url", input.returnUrl);
|
|
107
|
+
if (input.notifyUrl) params.set("metadata[notify_url]", input.notifyUrl);
|
|
108
|
+
|
|
109
|
+
const resp = await fetchWithRetry(`${STRIPE_API_BASE}/checkout/sessions`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
Authorization: `Bearer ${this.secretKey}`,
|
|
113
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
114
|
+
},
|
|
115
|
+
body: params.toString(),
|
|
116
|
+
timeoutMs: 10_000,
|
|
117
|
+
retries: 2,
|
|
118
|
+
});
|
|
119
|
+
if (!resp.ok) {
|
|
120
|
+
const errorBody = await resp.text();
|
|
121
|
+
throw new Error(`Stripe API error: ${resp.status} - ${errorBody}`);
|
|
122
|
+
}
|
|
123
|
+
const session = (await resp.json()) as { id: string; url?: string };
|
|
124
|
+
if (!session.url) throw new Error("Stripe returned no checkout URL");
|
|
125
|
+
return { redirectUrl: session.url, providerTradeNo: session.id };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 验证 Stripe webhook 回调。
|
|
130
|
+
*
|
|
131
|
+
* 路由层需传递 _raw_body(原始请求体)和 _stripe_signature(Stripe-Signature 头)。
|
|
132
|
+
*/
|
|
133
|
+
async verifyCallback(params: Record<string, string>): Promise<CallbackResult> {
|
|
134
|
+
const rawPayload = params["_raw_body"];
|
|
135
|
+
const sigHeader = params["_stripe_signature"];
|
|
136
|
+
if (!rawPayload || !sigHeader) {
|
|
137
|
+
throw new Error("Stripe callback missing signature headers");
|
|
138
|
+
}
|
|
139
|
+
if (!(await verifyStripeSignature(rawPayload, sigHeader, this.webhookSecret))) {
|
|
140
|
+
throw new Error("Stripe webhook signature invalid");
|
|
141
|
+
}
|
|
142
|
+
const event = JSON.parse(rawPayload) as {
|
|
143
|
+
type?: string;
|
|
144
|
+
data?: { object?: { metadata?: Record<string, string>; amount_total?: number; currency?: string; payment_intent?: string; id?: string } };
|
|
145
|
+
};
|
|
146
|
+
if (event.type !== "checkout.session.completed") {
|
|
147
|
+
throw new Error(`Unexpected Stripe event type: ${event.type}`);
|
|
148
|
+
}
|
|
149
|
+
const session = event.data?.object;
|
|
150
|
+
if (!session) throw new Error("Stripe event missing session object");
|
|
151
|
+
const orderNo = session.metadata?.order_no;
|
|
152
|
+
if (!orderNo) throw new Error("Stripe session missing order_no metadata");
|
|
153
|
+
return {
|
|
154
|
+
orderNo,
|
|
155
|
+
providerTradeNo: session.payment_intent || session.id || "",
|
|
156
|
+
amountCents: session.amount_total || 0,
|
|
157
|
+
currency: (session.currency || "usd").toUpperCase(),
|
|
158
|
+
paidAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 查询 Checkout Session 支付状态 */
|
|
163
|
+
async queryStatus(tradeNo: string): Promise<QueryStatusResult> {
|
|
164
|
+
const resp = await fetchWithRetry(`${STRIPE_API_BASE}/checkout/sessions/${tradeNo}`, {
|
|
165
|
+
headers: { Authorization: `Bearer ${this.secretKey}` },
|
|
166
|
+
timeoutMs: 8_000,
|
|
167
|
+
retries: 2,
|
|
168
|
+
});
|
|
169
|
+
if (!resp.ok) return { paid: false };
|
|
170
|
+
const session = (await resp.json()) as { payment_status?: string; payment_intent?: string };
|
|
171
|
+
return {
|
|
172
|
+
paid: session.payment_status === "paid",
|
|
173
|
+
providerTradeNo: session.payment_intent,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
// Stripe 工厂
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
181
|
+
|
|
182
|
+
export const stripeFactory: ProviderFactory = {
|
|
183
|
+
name: "stripe",
|
|
184
|
+
priority: 200,
|
|
185
|
+
isAvailable(env) { return !!(env.STRIPE_SECRET_KEY && env.STRIPE_WEBHOOK_SECRET); },
|
|
186
|
+
create(env) {
|
|
187
|
+
return new StripeProvider(
|
|
188
|
+
env.STRIPE_SECRET_KEY as string,
|
|
189
|
+
env.STRIPE_WEBHOOK_SECRET as string,
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
};
|