@h-ai/payment 0.1.0-alpha.66 → 0.1.0-alpha.68

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/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { paymentM, HaiPaymentError } from './chunk-4IUARFWH.js';
2
- export { HaiPaymentError } from './chunk-4IUARFWH.js';
1
+ import { paymentM, HaiPaymentError } from './chunk-YQD4KPT5.js';
2
+ export { HaiPaymentError } from './chunk-YQD4KPT5.js';
3
3
  import { z } from 'zod';
4
4
  import { core, err, ok } from '@h-ai/core';
5
+ import { reldb } from '@h-ai/reldb';
5
6
  import { audit } from '@h-ai/audit';
6
- import { createVerify, createDecipheriv, randomBytes, createSign, createHmac } from 'crypto';
7
+ import { createHash, createHmac, X509Certificate, createDecipheriv, randomBytes, createSign, createVerify } from 'crypto';
7
8
  import { Buffer } from 'buffer';
8
9
 
9
10
  var WechatPayConfigSchema = z.object({
@@ -11,21 +12,29 @@ var WechatPayConfigSchema = z.object({
11
12
  apiV3Key: z.string().min(1, paymentM("payment_configFieldRequired")),
12
13
  serialNo: z.string().min(1, paymentM("payment_configFieldRequired")),
13
14
  privateKey: z.string().min(1, paymentM("payment_configFieldRequired")),
14
- platformCert: z.string().optional(),
15
+ /** 精确按 Wechatpay-Serial 选择平台证书/公钥,轮换期间同时配置新旧标识。 */
16
+ platformKeys: z.record(z.string().min(1), z.string().min(1)).refine((keys) => Object.keys(keys).length > 0, paymentM("payment_configFieldRequired")),
15
17
  appId: z.string().min(1, paymentM("payment_configFieldRequired"))
16
18
  });
17
19
  var AlipayConfigSchema = z.object({
18
20
  appId: z.string().min(1, paymentM("payment_configFieldRequired")),
21
+ sellerId: z.string().min(1, paymentM("payment_configFieldRequired")),
19
22
  privateKey: z.string().min(1, paymentM("payment_configFieldRequired")),
20
23
  alipayPublicKey: z.string().min(1, paymentM("payment_configFieldRequired")),
24
+ /** 手机网站支付完成后返回的可信 HTTPS 地址;仅用于页面导航,不能证明付款。 */
25
+ returnUrl: z.url({ protocol: /^https$/ }),
21
26
  signType: z.enum(["RSA2", "RSA"]).default("RSA2"),
22
27
  sandbox: z.boolean().default(false)
23
28
  });
24
29
  var StripeConfigSchema = z.object({
25
- secretKey: z.string().min(1, paymentM("payment_configFieldRequired")),
30
+ secretKey: z.string().regex(/^(?:sk|rk)_(?:test|live)_\S+$/, paymentM("payment_stripeKeyInvalid")),
26
31
  webhookSecret: z.string().min(1, paymentM("payment_configFieldRequired")),
32
+ /** 托管 Checkout 完成后的可信跳转地址,可含 {CHECKOUT_SESSION_ID}。 */
33
+ successUrl: z.url({ protocol: /^https?$/ }),
34
+ /** 托管 Checkout 取消按钮的可信跳转地址;省略时不显示返回按钮。 */
35
+ cancelUrl: z.url({ protocol: /^https?$/ }).optional(),
27
36
  webhookToleranceSeconds: z.number().int().positive().default(300)
28
- });
37
+ }).strict();
29
38
  var PaymentConfigSchema = z.object({
30
39
  wechat: WechatPayConfigSchema.optional(),
31
40
  alipay: AlipayConfigSchema.optional(),
@@ -112,7 +121,7 @@ async function handleNotify(providerName, request) {
112
121
  action: "payment_notify",
113
122
  resource: "payment",
114
123
  resourceId: notifyResult.data.orderNo,
115
- details: { provider: providerName, transactionId: notifyResult.data.transactionId, status: notifyResult.data.status, amount: notifyResult.data.amount }
124
+ details: { provider: providerName, transactionId: notifyResult.data.transactionId, eventId: notifyResult.data.eventId, status: notifyResult.data.status, amount: notifyResult.data.amount, currency: notifyResult.data.currency }
116
125
  });
117
126
  }
118
127
  return notifyResult;
@@ -177,22 +186,77 @@ function signAlipayParams(params, privateKey, signType = "RSA2") {
177
186
  }
178
187
  function verifyAlipayNotify(params, alipayPublicKey) {
179
188
  const sign = params.sign ?? "";
180
- const signType = params.sign_type ?? "RSA2";
189
+ const signType = params.sign_type;
190
+ if (signType !== "RSA" && signType !== "RSA2")
191
+ return false;
181
192
  const sorted = Object.keys(params).filter((k) => k !== "sign" && k !== "sign_type" && params[k] !== void 0 && params[k] !== "").sort().map((k) => `${k}=${params[k]}`).join("&");
182
193
  const algorithm = signType === "RSA2" ? "RSA-SHA256" : "RSA-SHA1";
183
194
  const verify = createVerify(algorithm);
184
195
  verify.update(sorted);
185
196
  return verify.verify(alipayPublicKey, sign, "base64");
186
197
  }
198
+
199
+ // src/providers/alipay/payment-alipay-time.ts
200
+ var beijingFormatter = new Intl.DateTimeFormat("en-CA", {
201
+ timeZone: "Asia/Shanghai",
202
+ year: "numeric",
203
+ month: "2-digit",
204
+ day: "2-digit",
205
+ hour: "2-digit",
206
+ minute: "2-digit",
207
+ second: "2-digit",
208
+ hourCycle: "h23"
209
+ });
210
+ function formatAlipayTime(date) {
211
+ const parts = Object.fromEntries(beijingFormatter.formatToParts(date).map((part) => [part.type, part.value]));
212
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
213
+ }
214
+ function parseAlipayTime(value) {
215
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value))
216
+ return void 0;
217
+ const date = /* @__PURE__ */ new Date(`${value.replace(" ", "T")}+08:00`);
218
+ return Number.isFinite(date.getTime()) && formatAlipayTime(date) === value ? date : void 0;
219
+ }
220
+ var AlipayTimeSchema = z.string().refine((value) => parseAlipayTime(value) !== void 0);
221
+ var AlipayResponseRootSchema = z.record(z.string(), z.unknown());
222
+ var AlipayResponseSchema = z.object({
223
+ code: z.string().regex(/^\d{5}$/),
224
+ sub_code: z.string().optional()
225
+ });
226
+ var AlipayAmountSchema = z.string().regex(/^\d+(?:\.\d{1,2})?$/);
227
+ var AlipayQueryResponseSchema = z.object({
228
+ out_trade_no: z.string().min(1),
229
+ trade_no: z.string().min(1),
230
+ trade_status: z.enum(["WAIT_BUYER_PAY", "TRADE_CLOSED", "TRADE_SUCCESS", "TRADE_FINISHED"]),
231
+ total_amount: AlipayAmountSchema,
232
+ send_pay_date: AlipayTimeSchema.optional()
233
+ });
234
+ var AlipayRefundResponseSchema = z.object({
235
+ out_trade_no: z.string().min(1),
236
+ trade_no: z.string().min(1),
237
+ refund_fee: AlipayAmountSchema,
238
+ fund_change: z.enum(["Y", "N"]).optional()
239
+ });
240
+ var AlipayCloseResponseSchema = z.object({
241
+ out_trade_no: z.string().min(1),
242
+ trade_no: z.string().min(1)
243
+ });
187
244
  var AlipayNotifyParamsSchema = z.object({
245
+ app_id: z.string().min(1),
246
+ seller_id: z.string().min(1),
247
+ notify_id: z.string().min(1),
248
+ notify_type: z.literal("trade_status_sync"),
249
+ notify_time: AlipayTimeSchema,
250
+ sign_type: z.enum(["RSA", "RSA2"]),
251
+ sign: z.string().min(1),
188
252
  out_trade_no: z.string().min(1),
189
253
  trade_no: z.string().min(1),
190
- trade_status: z.string().min(1),
191
- total_amount: z.string().min(1),
192
- gmt_payment: z.string().optional()
193
- }).catchall(z.string());
254
+ trade_status: z.enum(["WAIT_BUYER_PAY", "TRADE_CLOSED", "TRADE_SUCCESS", "TRADE_FINISHED"]),
255
+ total_amount: AlipayAmountSchema,
256
+ gmt_payment: AlipayTimeSchema.optional()
257
+ }).catchall(z.string()).refine((params) => !["TRADE_SUCCESS", "TRADE_FINISHED"].includes(params.trade_status) || !!params.gmt_payment);
194
258
 
195
- // src/providers/alipay/alipay-provider.ts
259
+ // src/providers/alipay/payment-provider-alipay.ts
196
260
  var ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do";
197
261
  var ALIPAY_SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
198
262
  function formatFenAmount(amount) {
@@ -203,15 +267,22 @@ function formatFenAmount(amount) {
203
267
  function parseAlipayAmount(value) {
204
268
  const trimmedValue = value.trim();
205
269
  if (!/^\d+(?:\.\d{1,2})?$/.test(trimmedValue)) {
206
- throw new Error(`Invalid Alipay amount: ${value}`);
270
+ throw new Error("Invalid Alipay amount");
207
271
  }
208
272
  const [yuanPart, centsPart = ""] = trimmedValue.split(".");
209
- return Number.parseInt(yuanPart, 10) * 100 + Number.parseInt(centsPart.padEnd(2, "0"), 10);
273
+ const amount = Number.parseInt(yuanPart, 10) * 100 + Number.parseInt(centsPart.padEnd(2, "0"), 10);
274
+ if (!Number.isSafeInteger(amount)) {
275
+ throw new TypeError("Invalid Alipay amount");
276
+ }
277
+ return amount;
210
278
  }
211
279
  function parseAlipayNotifyParams(body) {
212
- const parseResult = AlipayNotifyParamsSchema.safeParse(Object.fromEntries(new URLSearchParams(body).entries()));
280
+ const params = new URLSearchParams(body);
281
+ if (new Set(params.keys()).size !== [...params].length)
282
+ throw new Error("Ambiguous Alipay notification parameters");
283
+ const parseResult = AlipayNotifyParamsSchema.safeParse(Object.fromEntries(params.entries()));
213
284
  if (!parseResult.success) {
214
- throw parseResult.error;
285
+ throw new Error("Invalid Alipay notification parameters");
215
286
  }
216
287
  return parseResult.data;
217
288
  }
@@ -222,59 +293,78 @@ function createAlipayProvider(config) {
222
293
  return {
223
294
  app_id: config.appId,
224
295
  method,
296
+ format: "JSON",
225
297
  charset: "utf-8",
226
298
  sign_type: signType,
227
- timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19),
299
+ timestamp: formatAlipayTime(/* @__PURE__ */ new Date()),
228
300
  version: "1.0",
229
301
  notify_url: notifyUrl
230
302
  };
231
303
  }
232
- function getTradeConfig(tradeType) {
233
- const map = {
234
- jsapi: { method: "alipay.trade.create", productCode: "JSAPI_PAY" },
235
- h5: { method: "alipay.trade.wap.pay", productCode: "QUICK_WAP_WAY" },
236
- app: { method: "alipay.trade.app.pay", productCode: "QUICK_MSECURITY_PAY" },
237
- native: { method: "alipay.trade.precreate", productCode: "FACE_TO_FACE_PAYMENT" }
238
- };
239
- return map[tradeType] ?? map.h5;
240
- }
241
- async function alipayRequest(params) {
304
+ async function alipayRequest(params, schema) {
242
305
  const sign = signAlipayParams(params, config.privateKey, signType);
243
306
  const allParams = { ...params, sign };
244
- const queryString = Object.entries(allParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
245
- const timedResponse = await fetchWithTimeout(`${gateway}?${queryString}`, {
307
+ const timedResponse = await fetchWithTimeout(gateway, {
246
308
  method: "POST",
247
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
309
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
310
+ body: new URLSearchParams(allParams).toString()
248
311
  });
249
312
  if (!timedResponse.ok) {
250
313
  throw new Error(`Alipay API failed: ${timedResponse.status}`);
251
314
  }
252
- return timedResponse.json();
315
+ let body;
316
+ try {
317
+ body = await timedResponse.json();
318
+ } catch {
319
+ throw new Error("Invalid Alipay JSON response");
320
+ }
321
+ const root = AlipayResponseRootSchema.safeParse(body);
322
+ const key = `${params.method.replace(/\./g, "_")}_response`;
323
+ if (!root.success || "error_response" in root.data) {
324
+ throw new Error("Invalid Alipay response envelope");
325
+ }
326
+ const response = root.data[key];
327
+ const common = AlipayResponseSchema.safeParse(response);
328
+ if (!common.success) {
329
+ throw new Error("Invalid Alipay response code");
330
+ }
331
+ if (common.data.code !== "10000") {
332
+ const subCode = common.data.sub_code;
333
+ const suffix = subCode && /^[\w.-]{1,80}$/.test(subCode) ? `, sub_code=${subCode}` : "";
334
+ throw new Error(`Alipay business request failed (code=${common.data.code}${suffix})`);
335
+ }
336
+ const parsed = schema.safeParse(response);
337
+ if (!parsed.success) {
338
+ throw new Error("Invalid Alipay business response");
339
+ }
340
+ return parsed.data;
253
341
  }
254
342
  return {
255
343
  name: "alipay",
256
344
  async createOrder(input) {
345
+ if (input.tradeType !== "h5")
346
+ return err(HaiPaymentError.UNSUPPORTED_TRADE_TYPE, paymentM("payment_unsupportedTradeType"));
347
+ if (input.currency !== void 0 && input.currency !== "CNY")
348
+ return err(HaiPaymentError.INVALID_INPUT, paymentM("payment_invalidCreateOrderInput"));
257
349
  try {
258
- const { method, productCode } = getTradeConfig(input.tradeType);
259
- const bizContent = JSON.stringify({
260
- out_trade_no: input.orderNo,
261
- total_amount: formatFenAmount(input.amount),
262
- subject: input.description,
263
- product_code: productCode,
264
- passback_params: input.metadata ? JSON.stringify(input.metadata) : void 0
265
- });
266
- const params = {
267
- ...buildCommonParams(method, input.notifyUrl),
268
- biz_content: bizContent
350
+ const fields = {
351
+ ...buildCommonParams("alipay.trade.wap.pay", input.notifyUrl),
352
+ return_url: config.returnUrl,
353
+ biz_content: JSON.stringify({
354
+ out_trade_no: input.orderNo,
355
+ total_amount: formatFenAmount(input.amount),
356
+ subject: input.description,
357
+ product_code: "QUICK_WAP_WAY",
358
+ passback_params: input.metadata ? encodeURIComponent(JSON.stringify(input.metadata)) : void 0
359
+ })
269
360
  };
270
- const response = await alipayRequest(params);
271
- const key = `${method.replace(/\./g, "_")}_response`;
272
- const data = response[key];
273
361
  return ok({
274
362
  provider: "alipay",
275
- tradeType: input.tradeType,
276
- clientParams: data ?? {},
277
- prepayId: data?.trade_no
363
+ tradeType: "h5",
364
+ clientParams: {
365
+ gateway,
366
+ fields: { ...fields, sign: signAlipayParams(fields, config.privateKey, signType) }
367
+ }
278
368
  });
279
369
  } catch (cause) {
280
370
  return err(
@@ -287,13 +377,21 @@ function createAlipayProvider(config) {
287
377
  async handleNotify(request) {
288
378
  try {
289
379
  const params = parseAlipayNotifyParams(request.body);
290
- const valid = verifyAlipayNotify(params, config.alipayPublicKey);
380
+ const valid = params.sign_type === signType && verifyAlipayNotify(params, config.alipayPublicKey);
291
381
  if (!valid) {
292
382
  return err(
293
383
  HaiPaymentError.NOTIFY_VERIFY_FAILED,
294
384
  paymentM("payment_notifyVerifyFailed")
295
385
  );
296
386
  }
387
+ if (params.app_id !== config.appId || params.seller_id !== config.sellerId) {
388
+ return err(HaiPaymentError.NOTIFY_VERIFY_FAILED, paymentM("payment_notifyVerifyFailed"));
389
+ }
390
+ const notifiedAt = parseAlipayTime(params.notify_time);
391
+ const paidAt = params.gmt_payment ? parseAlipayTime(params.gmt_payment) : void 0;
392
+ if (notifiedAt.getTime() > Date.now() + 3e5 || paidAt && paidAt.getTime() > notifiedAt.getTime() + 3e5) {
393
+ throw new Error("Invalid Alipay notification time");
394
+ }
297
395
  const statusMap = {
298
396
  TRADE_SUCCESS: "paid",
299
397
  TRADE_CLOSED: "closed",
@@ -303,10 +401,11 @@ function createAlipayProvider(config) {
303
401
  return ok({
304
402
  orderNo: params.out_trade_no,
305
403
  transactionId: params.trade_no,
404
+ eventId: params.notify_id,
306
405
  amount: parseAlipayAmount(params.total_amount),
307
- status: statusMap[params.trade_status] ?? "pending",
308
- paidAt: params.gmt_payment ? new Date(params.gmt_payment) : void 0,
309
- // 支付宝回调原始 params 的强类型为 AlipayNotifyParams,此处统一以 Record 暴露给上层做调试/审计
406
+ currency: "CNY",
407
+ status: statusMap[params.trade_status],
408
+ paidAt: ["TRADE_SUCCESS", "TRADE_FINISHED"].includes(params.trade_status) ? paidAt : void 0,
310
409
  raw: params
311
410
  });
312
411
  } catch (cause) {
@@ -324,8 +423,10 @@ function createAlipayProvider(config) {
324
423
  ...buildCommonParams(method, ""),
325
424
  biz_content: JSON.stringify({ out_trade_no: orderNo })
326
425
  };
327
- const response = await alipayRequest(params);
328
- const data = response.alipay_trade_query_response;
426
+ const data = await alipayRequest(params, AlipayQueryResponseSchema);
427
+ if (data.out_trade_no !== orderNo) {
428
+ throw new Error("Alipay response order mismatch");
429
+ }
329
430
  const statusMap = {
330
431
  TRADE_SUCCESS: "paid",
331
432
  TRADE_CLOSED: "closed",
@@ -334,10 +435,11 @@ function createAlipayProvider(config) {
334
435
  };
335
436
  return ok({
336
437
  orderNo,
337
- transactionId: data?.trade_no,
338
- status: statusMap[data?.trade_status] ?? "pending",
339
- amount: parseAlipayAmount(data?.total_amount ?? "0"),
340
- paidAt: data?.send_pay_date ? new Date(data.send_pay_date) : void 0
438
+ transactionId: data.trade_no,
439
+ status: statusMap[data.trade_status],
440
+ amount: parseAlipayAmount(data.total_amount),
441
+ // 支付宝时间为北京时间,不能依赖节点的本地时区解释。
442
+ paidAt: data.send_pay_date ? parseAlipayTime(data.send_pay_date) : void 0
341
443
  });
342
444
  } catch (cause) {
343
445
  return err(
@@ -359,12 +461,15 @@ function createAlipayProvider(config) {
359
461
  refund_reason: input.reason
360
462
  })
361
463
  };
362
- const response = await alipayRequest(params);
363
- const data = response.alipay_trade_refund_response;
464
+ const data = await alipayRequest(params, AlipayRefundResponseSchema);
465
+ if (data.out_trade_no !== input.orderNo) {
466
+ throw new Error("Alipay response order mismatch");
467
+ }
468
+ parseAlipayAmount(data.refund_fee);
364
469
  return ok({
365
470
  refundNo: input.refundNo,
366
- refundId: data?.trade_no ?? "",
367
- status: data?.fund_change === "Y" ? "success" : "processing"
471
+ refundId: data.trade_no,
472
+ status: data.fund_change === "Y" ? "success" : "processing"
368
473
  });
369
474
  } catch (cause) {
370
475
  return err(
@@ -381,7 +486,10 @@ function createAlipayProvider(config) {
381
486
  ...buildCommonParams(method, ""),
382
487
  biz_content: JSON.stringify({ out_trade_no: orderNo })
383
488
  };
384
- await alipayRequest(params);
489
+ const data = await alipayRequest(params, AlipayCloseResponseSchema);
490
+ if (data.out_trade_no !== orderNo) {
491
+ throw new Error("Alipay response order mismatch");
492
+ }
385
493
  return ok(void 0);
386
494
  } catch (cause) {
387
495
  return err(
@@ -393,16 +501,237 @@ function createAlipayProvider(config) {
393
501
  }
394
502
  };
395
503
  }
504
+
505
+ // src/providers/payment-provider-notify.ts
506
+ function getNotifyHeader(headers, name) {
507
+ const values = Object.entries(headers).filter(([key]) => key.toLowerCase() === name.toLowerCase());
508
+ return values.length === 1 ? values[0][1] : void 0;
509
+ }
510
+
511
+ // src/providers/stripe/payment-stripe-repository-operation.ts
512
+ var STRIPE_OPERATION_TABLE = "hai_payment_stripe_operations";
513
+ var SAFE_RETRY_SECONDS = 23 * 60 * 60;
514
+ var StripeOperationRepository = class {
515
+ constructor(db) {
516
+ this.db = db;
517
+ }
518
+ db;
519
+ /** 建表成功后才能发布 Provider;失败由 payment.init 返回。 */
520
+ async initialize() {
521
+ return this.db.ddl.createTable(STRIPE_OPERATION_TABLE, {
522
+ id: { type: "TEXT", primaryKey: true },
523
+ request_hash: { type: "TEXT", notNull: true },
524
+ result_json: { type: "JSON" },
525
+ payment_intent: { type: "TEXT" },
526
+ created_at: { type: "BIGINT", notNull: true }
527
+ }, true);
528
+ }
529
+ /** 数据库时间避免不同应用节点的系统时钟影响安全重试窗口。 */
530
+ nowSql() {
531
+ switch (this.db.config?.type) {
532
+ case "sqlite":
533
+ return "CAST(strftime('%s', 'now') AS INTEGER)";
534
+ case "postgresql":
535
+ return "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS BIGINT)";
536
+ case "mysql":
537
+ return "UNIX_TIMESTAMP()";
538
+ default:
539
+ throw new Error("Stripe operation repository requires an initialized database");
540
+ }
541
+ }
542
+ /**
543
+ * 读取持久操作记录,以共享数据库时间计算安全重试窗口,不根据渠道 metadata 列表猜订单。
544
+ * @param id - 按账户、模式、操作类型和业务标识生成的稳定摘要 ID。
545
+ * @returns 已保留的记录;无记录时为 null,result 为 null 仅代表结果未知。
546
+ * @throws 查询失败或持久结果 JSON 非法时抛错,由最近的 Provider 边界转为 HaiResult。
547
+ * @example 模块内部上下文
548
+ * ```ts
549
+ * async function lookup(repository: StripeOperationRepository, operationId: string) {
550
+ * const record = await repository.load(operationId)
551
+ * if (record?.result === null && !record.canRetry) {
552
+ * throw new Error('Reconcile the channel outcome before retrying')
553
+ * }
554
+ * return record
555
+ * }
556
+ * ```
557
+ */
558
+ async load(id) {
559
+ const result = await this.db.sql.get(
560
+ `SELECT request_hash, result_json, payment_intent,
561
+ CASE WHEN created_at > (${this.nowSql()} - ${SAFE_RETRY_SECONDS}) THEN 1 ELSE 0 END AS can_retry
562
+ FROM ${STRIPE_OPERATION_TABLE} WHERE id = ?`,
563
+ [id]
564
+ );
565
+ if (!result.success)
566
+ throw new Error("Failed to load Stripe operation", { cause: result.error });
567
+ if (!result.data)
568
+ return null;
569
+ const row = result.data;
570
+ return {
571
+ requestHash: row.request_hash,
572
+ result: typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json,
573
+ paymentIntent: row.payment_intent,
574
+ canRetry: row.can_retry === true || row.can_retry === 1 || row.can_retry === "1"
575
+ };
576
+ }
577
+ /**
578
+ * 通过主键插入原子预留;并发节点只接受相同请求摘要,不覆盖已有参数或删除未知操作。
579
+ * @param id - 所有节点复用的稳定操作 ID,也是渠道幂等键。
580
+ * @param requestHash - 路径和排序后表单参数的规范摘要。
581
+ * @returns 数据库中唯一的操作记录;已有 result 必须复用而非重新请求渠道。
582
+ * @throws 预留/读取失败或同标识复用不同参数时抛错,由 Provider 转为操作失败。
583
+ * @example 模块内部上下文
584
+ * ```ts
585
+ * async function reserveOnce(repository: StripeOperationRepository, id: string, requestHash: string) {
586
+ * const record = await repository.reserve(id, requestHash)
587
+ * // result=null 可能是渠道执行后回执丢失;重试仍须原 id 且 canRetry=true。
588
+ * if (record.result === null && !record.canRetry) throw new Error('Channel reconciliation required')
589
+ * return record
590
+ * }
591
+ * ```
592
+ */
593
+ async reserve(id, requestHash) {
594
+ let record = await this.load(id);
595
+ if (!record) {
596
+ const inserted = await this.db.sql.execute(
597
+ `INSERT INTO ${STRIPE_OPERATION_TABLE} (id, request_hash, created_at) VALUES (?, ?, ${this.nowSql()})`,
598
+ [id, requestHash]
599
+ );
600
+ record = await this.load(id);
601
+ if (!record)
602
+ throw new Error("Failed to reserve Stripe operation", { cause: inserted.success ? void 0 : inserted.error });
603
+ }
604
+ if (record.requestHash !== requestHash)
605
+ throw new Error("Stripe operation identifier was reused with different parameters");
606
+ return record;
607
+ }
608
+ /**
609
+ * 以请求摘要和未完成状态作 CAS 发布渠道结果,再读取已持久化的唯一结果。
610
+ * @param id - 已成功预留的稳定操作 ID。
611
+ * @param requestHash - 与预留完全一致的规范请求摘要。
612
+ * @param result - 已经由渠道结果 Schema 校验、允许持久化的最小结果。
613
+ * @param paymentIntent - 已核对身份的 PaymentIntent;创建时尚未知可省略。
614
+ * @returns 数据库确认的结果,可能由并发完成者先写入;调用者须再按结果 Schema 解析。
615
+ * @throws 更新、重读或持久状态核对失败时抛错;不将未知状态伪装为成功。
616
+ * @example 模块内部上下文
617
+ * ```ts
618
+ * async function persistConfirmed(repository: StripeOperationRepository, id: string, digest: string, confirmedMapping: unknown) {
619
+ * // 调用前渠道响应已校验;digest 来自首次预留,不能以新参数覆盖旧操作。
620
+ * return repository.complete(id, digest, confirmedMapping)
621
+ * }
622
+ * ```
623
+ */
624
+ async complete(id, requestHash, result, paymentIntent) {
625
+ const updated = await this.db.sql.execute(
626
+ `UPDATE ${STRIPE_OPERATION_TABLE} SET result_json = ?, payment_intent = ?
627
+ WHERE id = ? AND request_hash = ? AND result_json IS NULL`,
628
+ [JSON.stringify(result), paymentIntent ?? null, id, requestHash]
629
+ );
630
+ if (!updated.success)
631
+ throw new Error("Failed to persist Stripe operation result", { cause: updated.error });
632
+ const persisted = await this.load(id);
633
+ if (!persisted || persisted.requestHash !== requestHash || persisted.result === null)
634
+ throw new Error("Stripe operation result was not persisted");
635
+ return persisted.result;
636
+ }
637
+ /**
638
+ * 只在订单映射已完成时首次补全或幂等确认同一 PaymentIntent,禁止迟到写覆盖渠道身份。
639
+ * @param id - 已确认 Checkout Session 映射的订单操作 ID。
640
+ * @param paymentIntent - 从该 Session 读取且通过订单/金额/币种核对的渠道标识。
641
+ * @throws 写入失败、订单未完成或已绑定不同 PaymentIntent 时抛错,由 Provider 转为失败。
642
+ * @example 模块内部上下文
643
+ * ```ts
644
+ * async function bindVerifiedIntent(repository: StripeOperationRepository, orderOperationId: string, verifiedIntentId: string) {
645
+ * await repository.rememberPaymentIntent(orderOperationId, verifiedIntentId)
646
+ * // 仅确认渠道身份映射,不代表宿主业务已履约。
647
+ * }
648
+ * ```
649
+ */
650
+ async rememberPaymentIntent(id, paymentIntent) {
651
+ const result = await this.db.sql.execute(
652
+ `UPDATE ${STRIPE_OPERATION_TABLE} SET payment_intent = ?
653
+ WHERE id = ? AND result_json IS NOT NULL AND (payment_intent IS NULL OR payment_intent = ?)`,
654
+ [paymentIntent, id, paymentIntent]
655
+ );
656
+ if (!result.success)
657
+ throw new Error("Failed to persist Stripe PaymentIntent", { cause: result.error });
658
+ const record = await this.load(id);
659
+ if (!record || record.paymentIntent !== paymentIntent)
660
+ throw new Error("Stripe PaymentIntent conflicts with the persisted mapping");
661
+ }
662
+ };
663
+
664
+ // src/providers/stripe/payment-provider-stripe.ts
396
665
  var STRIPE_API_BASE = "https://api.stripe.com/v1";
397
- function createStripeProvider(config) {
398
- async function stripeRequest(method, path, body) {
666
+ var STRIPE_API_VERSION = "2026-09-30.endive";
667
+ var StripeCreatedSessionSchema = z.object({
668
+ id: z.string().min(1),
669
+ url: z.url(),
670
+ payment_intent: z.string().min(1).nullable().optional()
671
+ });
672
+ var StripeOrderMappingSchema = z.object({
673
+ orderNo: z.string().min(1),
674
+ amount: z.number().int().positive(),
675
+ currency: z.string().regex(/^[A-Z]{3}$/),
676
+ session: StripeCreatedSessionSchema
677
+ });
678
+ var StripeRetrievedSessionSchema = z.object({
679
+ id: z.string().min(1),
680
+ metadata: z.object({ orderNo: z.string().min(1) }),
681
+ amount_total: z.number().int().nonnegative(),
682
+ currency: z.string().regex(/^[a-z]{3}$/i),
683
+ payment_status: z.enum(["paid", "unpaid", "no_payment_required"]),
684
+ payment_intent: z.string().min(1).nullable(),
685
+ status: z.enum(["open", "complete", "expired"])
686
+ });
687
+ var StripeRefundSchema = z.object({
688
+ id: z.string().min(1),
689
+ status: z.enum(["pending", "requires_action", "succeeded", "failed", "canceled"])
690
+ });
691
+ var StripeCheckoutEventSchema = z.object({
692
+ id: z.string().min(1),
693
+ livemode: z.boolean(),
694
+ // 本 Provider 只支持当前账户,不能消费 Connect/组织上下文的其它商户事件。
695
+ account: z.never().optional(),
696
+ context: z.never().optional(),
697
+ created: z.number().int().nonnegative().max(864e10),
698
+ type: z.enum([
699
+ "checkout.session.completed",
700
+ "checkout.session.async_payment_succeeded",
701
+ "checkout.session.async_payment_failed",
702
+ "checkout.session.expired"
703
+ ]),
704
+ data: z.object({
705
+ object: z.object({
706
+ object: z.literal("checkout.session"),
707
+ id: z.string().min(1),
708
+ mode: z.literal("payment"),
709
+ metadata: z.object({ orderNo: z.string().min(1).regex(/\S/) }).passthrough(),
710
+ amount_total: z.number().int().nonnegative(),
711
+ currency: z.string().regex(/^[a-z]{3}$/i),
712
+ payment_status: z.enum(["paid", "unpaid", "no_payment_required"])
713
+ }).passthrough()
714
+ }).passthrough()
715
+ }).passthrough().refine((event) => {
716
+ const paid = event.data.object.payment_status !== "unpaid";
717
+ if (event.type === "checkout.session.async_payment_succeeded")
718
+ return paid;
719
+ if (event.type === "checkout.session.async_payment_failed" || event.type === "checkout.session.expired")
720
+ return !paid;
721
+ return true;
722
+ }, { message: "Checkout event type contradicts payment status" });
723
+ function createStripeProvider(config, repository = new StripeOperationRepository(reldb)) {
724
+ let accountScopePromise;
725
+ async function stripeRequest(method, path, options) {
399
726
  const response = await fetchWithTimeout(`${STRIPE_API_BASE}${path}`, {
400
727
  method,
401
728
  headers: {
402
729
  "Authorization": `Bearer ${config.secretKey}`,
403
- "Content-Type": "application/x-www-form-urlencoded"
730
+ "Content-Type": "application/x-www-form-urlencoded",
731
+ "Stripe-Version": STRIPE_API_VERSION,
732
+ ...options?.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : {}
404
733
  },
405
- body: body ? Object.entries(body).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : void 0
734
+ body: options?.body ? Object.entries(options.body).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : void 0
406
735
  });
407
736
  if (!response.ok) {
408
737
  const errorText = await response.text();
@@ -410,19 +739,68 @@ function createStripeProvider(config) {
410
739
  }
411
740
  return response.json();
412
741
  }
742
+ async function accountScope() {
743
+ if (!accountScopePromise) {
744
+ accountScopePromise = (async () => {
745
+ const mode = /^(?:sk|rk)_(test|live)_/.exec(config.secretKey)?.[1];
746
+ if (!mode)
747
+ throw new Error("Stripe requires a test or live secret/restricted API key");
748
+ const account = z.object({ id: z.string().regex(/^acct_/).min(6) }).parse(await stripeRequest("GET", "/account"));
749
+ return `${account.id}:${mode}`;
750
+ })().catch((error) => {
751
+ accountScopePromise = void 0;
752
+ throw error;
753
+ });
754
+ }
755
+ return accountScopePromise;
756
+ }
757
+ async function operationId(kind, businessId) {
758
+ const digest = createHash("sha256").update(JSON.stringify([await accountScope(), kind, businessId])).digest("hex");
759
+ return `hai-payment-${kind}-${digest}`;
760
+ }
761
+ async function executeOperation(options) {
762
+ const requestHash = createHash("sha256").update(JSON.stringify([
763
+ options.path,
764
+ Object.entries(options.body).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
765
+ ])).digest("hex");
766
+ const reserved = await repository.reserve(options.id, requestHash);
767
+ if (reserved.result !== null)
768
+ return options.resultSchema.parse(reserved.result);
769
+ if (!reserved.canRetry)
770
+ throw new Error("Stripe operation outcome is unknown beyond the safe retry window; reconcile before retrying");
771
+ const result = options.execute(await stripeRequest("POST", options.path, { body: options.body, idempotencyKey: options.id }));
772
+ return options.resultSchema.parse(await repository.complete(options.id, requestHash, result, options.paymentIntent?.(result)));
773
+ }
774
+ async function retrieveOrder(orderNo) {
775
+ const id = await operationId("order", orderNo);
776
+ const operation = await repository.load(id);
777
+ if (!operation || operation.result === null)
778
+ throw new Error("Stripe order has no confirmed persisted Session mapping");
779
+ const mapping = StripeOrderMappingSchema.parse(operation.result);
780
+ const session = StripeRetrievedSessionSchema.parse(await stripeRequest("GET", `/checkout/sessions/${encodeURIComponent(mapping.session.id)}`));
781
+ if (session.id !== mapping.session.id || session.metadata.orderNo !== orderNo || mapping.orderNo !== orderNo || session.amount_total !== mapping.amount || session.currency.toUpperCase() !== mapping.currency) {
782
+ throw new Error("Stripe Session does not match the persisted order");
783
+ }
784
+ if (session.payment_intent)
785
+ await repository.rememberPaymentIntent(id, session.payment_intent);
786
+ return session;
787
+ }
413
788
  const TIMESTAMP_TOLERANCE = config.webhookToleranceSeconds ?? 300;
414
789
  function verifyWebhookSignature(payload, signature) {
415
- const parts = signature.split(",");
416
- const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2) ?? "";
417
- const v1 = parts.find((p) => p.startsWith("v1="))?.slice(3) ?? "";
418
- const ts = Number.parseInt(timestamp, 10);
419
- if (Number.isNaN(ts) || Math.abs(Math.floor(Date.now() / 1e3) - ts) > TIMESTAMP_TOLERANCE) {
790
+ const parts = signature.split(",").map((part) => part.trim());
791
+ const timestamps = parts.filter((p) => p.startsWith("t="));
792
+ if (timestamps.length !== 1)
793
+ return false;
794
+ const timestamp = timestamps[0].slice(2);
795
+ const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
796
+ const ts = Number(timestamp);
797
+ if (!/^(?:0|[1-9]\d{0,11})$/.test(timestamp) || !Number.isSafeInteger(ts) || Math.abs(Math.floor(Date.now() / 1e3) - ts) > TIMESTAMP_TOLERANCE) {
420
798
  return false;
421
799
  }
422
800
  const signedPayload = `${timestamp}.${payload}`;
423
801
  const expected = createHmac("sha256", config.webhookSecret).update(signedPayload).digest("hex");
424
802
  try {
425
- return core.string.constantTimeEqual(v1, expected);
803
+ return signatures.some((v1) => /^[a-f0-9]{64}$/.test(v1) && core.string.constantTimeEqual(v1, expected));
426
804
  } catch {
427
805
  return false;
428
806
  }
@@ -430,27 +808,48 @@ function createStripeProvider(config) {
430
808
  return {
431
809
  name: "stripe",
432
810
  async createOrder(input) {
811
+ if (input.tradeType !== "h5")
812
+ return err(HaiPaymentError.UNSUPPORTED_TRADE_TYPE, paymentM("payment_unsupportedTradeType"));
433
813
  try {
814
+ z.string().min(1).regex(/\S/).parse(input.orderNo);
815
+ if (input.metadata && Object.hasOwn(input.metadata, "orderNo"))
816
+ throw new Error("Stripe metadata orderNo is reserved");
434
817
  const params = {
435
818
  "mode": "payment",
819
+ "ui_mode": "hosted_page",
820
+ "success_url": config.successUrl,
436
821
  "line_items[0][price_data][currency]": input.currency?.toLowerCase() ?? "usd",
437
822
  "line_items[0][price_data][product_data][name]": input.description,
438
823
  "line_items[0][price_data][unit_amount]": input.amount.toString(),
439
824
  "line_items[0][quantity]": "1"
440
825
  };
826
+ if (config.cancelUrl)
827
+ params.cancel_url = config.cancelUrl;
441
828
  params["metadata[orderNo]"] = input.orderNo;
442
829
  if (input.metadata) {
443
830
  for (const [k, v] of Object.entries(input.metadata)) {
444
831
  params[`metadata[${k}]`] = v;
445
832
  }
446
833
  }
447
- const session = await stripeRequest("POST", "/checkout/sessions", params);
834
+ const mapping = await executeOperation({
835
+ id: await operationId("order", input.orderNo),
836
+ path: "/checkout/sessions",
837
+ body: params,
838
+ resultSchema: StripeOrderMappingSchema,
839
+ execute: (raw) => ({
840
+ orderNo: input.orderNo,
841
+ amount: input.amount,
842
+ currency: input.currency?.toUpperCase() ?? "USD",
843
+ session: StripeCreatedSessionSchema.parse(raw)
844
+ }),
845
+ paymentIntent: (result) => result.session.payment_intent
846
+ });
448
847
  return ok({
449
848
  provider: "stripe",
450
849
  tradeType: input.tradeType,
451
850
  clientParams: {
452
- sessionId: session.id,
453
- checkoutUrl: session.url
851
+ sessionId: mapping.session.id,
852
+ checkoutUrl: mapping.session.url
454
853
  }
455
854
  });
456
855
  } catch (cause) {
@@ -463,7 +862,7 @@ function createStripeProvider(config) {
463
862
  },
464
863
  async handleNotify(request) {
465
864
  try {
466
- const signature = request.headers["stripe-signature"] ?? "";
865
+ const signature = getNotifyHeader(request.headers, "stripe-signature") ?? "";
467
866
  const valid = verifyWebhookSignature(request.body, signature);
468
867
  if (!valid) {
469
868
  return err(
@@ -471,41 +870,37 @@ function createStripeProvider(config) {
471
870
  paymentM("payment_notifyVerifyFailed")
472
871
  );
473
872
  }
474
- const event = JSON.parse(request.body);
873
+ const parsed = StripeCheckoutEventSchema.safeParse(JSON.parse(request.body));
874
+ if (!parsed.success)
875
+ throw new Error("Invalid Stripe payment event");
876
+ const event = parsed.data;
877
+ if (event.livemode !== /^(?:sk|rk)_live_/.test(config.secretKey) || event.created > Math.floor(Date.now() / 1e3) + TIMESTAMP_TOLERANCE) {
878
+ return err(HaiPaymentError.NOTIFY_VERIFY_FAILED, paymentM("payment_notifyVerifyFailed"));
879
+ }
475
880
  const obj = event.data.object;
476
- const orderNo = obj.metadata?.orderNo ?? "";
477
- const statusMap = {
478
- "checkout.session.completed": "paid",
479
- "payment_intent.payment_failed": "failed"
480
- };
881
+ const status = event.type === "checkout.session.expired" ? "closed" : event.type === "checkout.session.async_payment_failed" ? "failed" : obj.payment_status === "unpaid" ? "pending" : "paid";
481
882
  return ok({
482
- orderNo,
883
+ orderNo: obj.metadata.orderNo,
483
884
  transactionId: obj.id,
484
- amount: obj.amount_total ?? 0,
485
- status: statusMap[event.type] ?? "pending",
486
- paidAt: /* @__PURE__ */ new Date(),
487
- // Stripe SDK 的 Event 是强类型对象,此处统一以 Record 暴露给上层做调试/审计
885
+ eventId: event.id,
886
+ amount: obj.amount_total,
887
+ currency: obj.currency.toUpperCase(),
888
+ status,
889
+ // 使用支付成功事件的时间,使重复通知不会改变 paidAt;pending/failed/closed 不宣称已支付。
890
+ paidAt: status === "paid" ? new Date(event.created * 1e3) : void 0,
488
891
  raw: event
489
892
  });
490
- } catch (cause) {
893
+ } catch {
491
894
  return err(
492
895
  HaiPaymentError.NOTIFY_PARSE_FAILED,
493
896
  paymentM("payment_notifyParseFailed"),
494
- cause
897
+ new Error("Invalid Stripe payment notification")
495
898
  );
496
899
  }
497
900
  },
498
901
  async queryOrder(orderNo) {
499
902
  try {
500
- const sessions = await stripeRequest("GET", `/checkout/sessions?limit=1&metadata[orderNo]=${encodeURIComponent(orderNo)}`);
501
- const session = sessions.data[0];
502
- if (!session) {
503
- return ok({
504
- orderNo,
505
- status: "pending",
506
- amount: 0
507
- });
508
- }
903
+ const session = await retrieveOrder(orderNo);
509
904
  const statusMap = {
510
905
  paid: "paid",
511
906
  unpaid: "pending",
@@ -514,7 +909,7 @@ function createStripeProvider(config) {
514
909
  return ok({
515
910
  orderNo,
516
911
  transactionId: session.id,
517
- status: statusMap[session.payment_status] ?? "pending",
912
+ status: session.status === "expired" ? "closed" : statusMap[session.payment_status],
518
913
  amount: session.amount_total
519
914
  });
520
915
  } catch (cause) {
@@ -527,23 +922,34 @@ function createStripeProvider(config) {
527
922
  },
528
923
  async refund(input) {
529
924
  try {
530
- const sessions = await stripeRequest("GET", `/checkout/sessions?limit=1&metadata[orderNo]=${encodeURIComponent(input.orderNo)}`);
531
- const paymentIntent = sessions.data[0]?.payment_intent;
532
- if (!paymentIntent) {
925
+ z.string().min(1).regex(/\S/).parse(input.refundNo);
926
+ const session = await retrieveOrder(input.orderNo);
927
+ if (!session.payment_intent || session.payment_status !== "paid") {
533
928
  return err(
534
929
  HaiPaymentError.REFUND_FAILED,
535
930
  paymentM("payment_refundFailed")
536
931
  );
537
932
  }
538
- const refund2 = await stripeRequest("POST", "/refunds", {
539
- payment_intent: paymentIntent,
540
- amount: input.amount.toString(),
541
- reason: input.reason ?? "requested_by_customer"
933
+ const body = {
934
+ "payment_intent": session.payment_intent,
935
+ "amount": input.amount.toString(),
936
+ "reason": "requested_by_customer",
937
+ "metadata[orderNo]": input.orderNo,
938
+ "metadata[refundNo]": input.refundNo
939
+ };
940
+ if (input.reason)
941
+ body["metadata[reason]"] = input.reason;
942
+ const refund2 = await executeOperation({
943
+ id: await operationId("refund", input.refundNo),
944
+ path: "/refunds",
945
+ body,
946
+ resultSchema: StripeRefundSchema,
947
+ execute: (raw) => StripeRefundSchema.parse(raw)
542
948
  });
543
949
  return ok({
544
950
  refundNo: input.refundNo,
545
951
  refundId: refund2.id,
546
- status: refund2.status === "succeeded" ? "success" : "processing"
952
+ status: refund2.status === "succeeded" ? "success" : refund2.status === "failed" || refund2.status === "canceled" ? "failed" : "processing"
547
953
  });
548
954
  } catch (cause) {
549
955
  return err(
@@ -553,8 +959,31 @@ function createStripeProvider(config) {
553
959
  );
554
960
  }
555
961
  },
556
- async closeOrder(_orderNo) {
557
- return ok(void 0);
962
+ async closeOrder(orderNo) {
963
+ try {
964
+ const session = await retrieveOrder(orderNo);
965
+ if (session.payment_status !== "unpaid" || session.status === "complete")
966
+ throw new Error("A completed or paid Stripe Session cannot be closed");
967
+ if (session.status === "expired")
968
+ return ok(void 0);
969
+ try {
970
+ const expired = z.object({ id: z.string(), status: z.literal("expired") }).parse(
971
+ await stripeRequest("POST", `/checkout/sessions/${encodeURIComponent(session.id)}/expire`, {
972
+ body: {},
973
+ idempotencyKey: await operationId("close", orderNo)
974
+ })
975
+ );
976
+ if (expired.id !== session.id)
977
+ throw new Error("Stripe expiration response does not match the requested Session");
978
+ } catch (cause) {
979
+ const confirmed = await retrieveOrder(orderNo);
980
+ if (confirmed.status !== "expired" || confirmed.payment_status !== "unpaid")
981
+ throw cause;
982
+ }
983
+ return ok(void 0);
984
+ } catch (cause) {
985
+ return err(HaiPaymentError.CLOSE_ORDER_FAILED, paymentM("payment_closeOrderFailed"), cause);
986
+ }
558
987
  }
559
988
  };
560
989
  }
@@ -585,14 +1014,14 @@ prepay_id=${prepayId}
585
1014
  sign.update(message);
586
1015
  return sign.sign(privateKey, "base64");
587
1016
  }
588
- function verifyNotifySignature(timestamp, nonce, body, signature, platformCert) {
1017
+ function verifyNotifySignature(timestamp, nonce, body, signature, platformKey) {
589
1018
  const message = `${timestamp}
590
1019
  ${nonce}
591
1020
  ${body}
592
1021
  `;
593
1022
  const verify = createVerify("RSA-SHA256");
594
1023
  verify.update(message);
595
- return verify.verify(platformCert, signature, "base64");
1024
+ return verify.verify(platformKey, signature, "base64");
596
1025
  }
597
1026
  function decryptResource(ciphertext, nonce, associatedData, apiV3Key) {
598
1027
  const buf = Buffer.from(ciphertext, "base64");
@@ -604,10 +1033,60 @@ function decryptResource(ciphertext, nonce, associatedData, apiV3Key) {
604
1033
  const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
605
1034
  return decrypted.toString("utf-8");
606
1035
  }
1036
+ var WechatNotifyEnvelopeSchema = z.object({
1037
+ id: z.string().min(1),
1038
+ create_time: z.iso.datetime({ offset: true }),
1039
+ event_type: z.literal("TRANSACTION.SUCCESS"),
1040
+ resource_type: z.literal("encrypt-resource"),
1041
+ resource: z.object({
1042
+ algorithm: z.literal("AEAD_AES_256_GCM"),
1043
+ original_type: z.literal("transaction"),
1044
+ ciphertext: z.string().min(1),
1045
+ nonce: z.string().min(1),
1046
+ associated_data: z.string().optional()
1047
+ })
1048
+ });
1049
+ var WechatNotifyResourceSchema = z.object({
1050
+ appid: z.string().min(1),
1051
+ mchid: z.string().min(1),
1052
+ out_trade_no: z.string().min(1),
1053
+ transaction_id: z.string().min(1),
1054
+ trade_state: z.literal("SUCCESS"),
1055
+ amount: z.object({
1056
+ total: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
1057
+ currency: z.string().regex(/^[A-Z]{3}$/)
1058
+ }).passthrough(),
1059
+ success_time: z.iso.datetime({ offset: true })
1060
+ }).passthrough();
607
1061
 
608
- // src/providers/wechat/wechat-pay-provider.ts
1062
+ // src/providers/wechat/payment-provider-wechat.ts
609
1063
  var WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
610
1064
  function createWechatPayProvider(config) {
1065
+ function verifyNotification(request) {
1066
+ try {
1067
+ const timestamp = getNotifyHeader(request.headers, "wechatpay-timestamp") ?? "";
1068
+ const nonce = getNotifyHeader(request.headers, "wechatpay-nonce") ?? "";
1069
+ const signature = getNotifyHeader(request.headers, "wechatpay-signature") ?? "";
1070
+ const serial = getNotifyHeader(request.headers, "wechatpay-serial") ?? "";
1071
+ const signatureType = getNotifyHeader(request.headers, "wechatpay-signature-type");
1072
+ const now = Date.now();
1073
+ if (!/^(?:0|[1-9]\d{0,11})$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp)) || Math.abs(Math.floor(now / 1e3) - Number(timestamp)) > 300 || !nonce || !signature || !serial || signatureType !== void 0 && signatureType !== "WECHATPAY2-SHA256-RSA2048") {
1074
+ return false;
1075
+ }
1076
+ const key = Object.hasOwn(config.platformKeys, serial) ? config.platformKeys[serial] : void 0;
1077
+ if (!key)
1078
+ return false;
1079
+ if (key.includes("-----BEGIN CERTIFICATE-----")) {
1080
+ const certificate = new X509Certificate(key);
1081
+ if (certificate.serialNumber.toUpperCase() !== serial.toUpperCase() || now < Date.parse(certificate.validFrom) || now > Date.parse(certificate.validTo)) {
1082
+ return false;
1083
+ }
1084
+ }
1085
+ return verifyNotifySignature(timestamp, nonce, request.body, signature, key);
1086
+ } catch {
1087
+ return false;
1088
+ }
1089
+ }
611
1090
  async function wechatRequest(method, path, body) {
612
1091
  const timestamp = getTimestamp();
613
1092
  const nonce = generateNonce();
@@ -629,18 +1108,14 @@ function createWechatPayProvider(config) {
629
1108
  }
630
1109
  return response.json();
631
1110
  }
632
- function getOrderPath(tradeType) {
633
- const map = {
634
- jsapi: "/v3/pay/transactions/jsapi",
635
- h5: "/v3/pay/transactions/h5",
636
- app: "/v3/pay/transactions/app",
637
- native: "/v3/pay/transactions/native"
638
- };
639
- return map[tradeType] ?? "/v3/pay/transactions/jsapi";
640
- }
641
1111
  return {
642
1112
  name: "wechat",
643
1113
  async createOrder(input) {
1114
+ if (input.tradeType !== "jsapi")
1115
+ return err(HaiPaymentError.UNSUPPORTED_TRADE_TYPE, paymentM("payment_unsupportedTradeType"));
1116
+ if (typeof input.userId !== "string" || !input.userId.trim() || input.userId.length > 128 || input.currency !== void 0 && input.currency !== "CNY") {
1117
+ return err(HaiPaymentError.INVALID_INPUT, paymentM("payment_invalidCreateOrderInput"));
1118
+ }
644
1119
  try {
645
1120
  const requestBody = {
646
1121
  appid: config.appId,
@@ -650,35 +1125,26 @@ function createWechatPayProvider(config) {
650
1125
  notify_url: input.notifyUrl,
651
1126
  amount: {
652
1127
  total: input.amount,
653
- currency: input.currency ?? "CNY"
654
- }
1128
+ currency: "CNY"
1129
+ },
1130
+ payer: { openid: input.userId }
655
1131
  };
656
- if (input.tradeType === "jsapi" && input.userId) {
657
- requestBody.payer = { openid: input.userId };
658
- }
659
1132
  if (input.metadata) {
660
1133
  requestBody.attach = JSON.stringify(input.metadata);
661
1134
  }
662
- const path = getOrderPath(input.tradeType);
663
- const response = await wechatRequest("POST", path, requestBody);
664
- let clientParams = {};
665
- if (input.tradeType === "jsapi") {
666
- const timestamp = getTimestamp();
667
- const nonce = generateNonce();
668
- const paySign = signJsapi(config.appId, timestamp, nonce, response.prepay_id, config.privateKey);
669
- clientParams = {
670
- appId: config.appId,
671
- timeStamp: timestamp,
672
- nonceStr: nonce,
673
- package: `prepay_id=${response.prepay_id}`,
674
- signType: "RSA",
675
- paySign
676
- };
677
- } else if (input.tradeType === "h5") {
678
- clientParams = { h5Url: response.h5_url };
679
- } else if (input.tradeType === "native") {
680
- clientParams = { codeUrl: response.code_url };
681
- }
1135
+ const response = await wechatRequest("POST", "/v3/pay/transactions/jsapi", requestBody);
1136
+ if (typeof response.prepay_id !== "string" || !response.prepay_id.trim())
1137
+ throw new Error("Invalid Wechat JSAPI prepay response");
1138
+ const timestamp = getTimestamp();
1139
+ const nonce = generateNonce();
1140
+ const clientParams = {
1141
+ appId: config.appId,
1142
+ timeStamp: timestamp,
1143
+ nonceStr: nonce,
1144
+ package: `prepay_id=${response.prepay_id}`,
1145
+ signType: "RSA",
1146
+ paySign: signJsapi(config.appId, timestamp, nonce, response.prepay_id, config.privateKey)
1147
+ };
682
1148
  return ok({
683
1149
  provider: "wechat",
684
1150
  tradeType: input.tradeType,
@@ -695,44 +1161,44 @@ function createWechatPayProvider(config) {
695
1161
  },
696
1162
  async handleNotify(request) {
697
1163
  try {
698
- const timestamp = request.headers["wechatpay-timestamp"] ?? "";
699
- const nonce = request.headers["wechatpay-nonce"] ?? "";
700
- const signature = request.headers["wechatpay-signature"] ?? "";
701
- if (!config.platformCert) {
702
- return err(
703
- HaiPaymentError.NOTIFY_VERIFY_FAILED,
704
- paymentM("payment_notifyVerifyFailed")
705
- );
706
- }
707
- const valid = verifyNotifySignature(timestamp, nonce, request.body, signature, config.platformCert);
708
- if (!valid) {
1164
+ if (!verifyNotification(request)) {
709
1165
  return err(
710
1166
  HaiPaymentError.NOTIFY_VERIFY_FAILED,
711
1167
  paymentM("payment_notifyVerifyFailed")
712
1168
  );
713
1169
  }
714
- const body = JSON.parse(request.body);
1170
+ const parsedBody = WechatNotifyEnvelopeSchema.safeParse(JSON.parse(request.body));
1171
+ if (!parsedBody.success)
1172
+ throw new Error("Invalid Wechat payment notification");
1173
+ const body = parsedBody.data;
715
1174
  const decrypted = decryptResource(
716
1175
  body.resource.ciphertext,
717
1176
  body.resource.nonce,
718
- body.resource.associated_data,
1177
+ body.resource.associated_data ?? "",
719
1178
  config.apiV3Key
720
1179
  );
721
- const resource = JSON.parse(decrypted);
1180
+ const parsedResource = WechatNotifyResourceSchema.safeParse(JSON.parse(decrypted));
1181
+ if (!parsedResource.success)
1182
+ throw new Error("Invalid Wechat payment notification resource");
1183
+ const resource = parsedResource.data;
1184
+ if (resource.appid !== config.appId || resource.mchid !== config.mchId) {
1185
+ return err(HaiPaymentError.NOTIFY_VERIFY_FAILED, paymentM("payment_notifyVerifyFailed"));
1186
+ }
722
1187
  return ok({
723
1188
  orderNo: resource.out_trade_no,
724
1189
  transactionId: resource.transaction_id,
1190
+ eventId: body.id,
725
1191
  amount: resource.amount.total,
726
- status: resource.trade_state === "SUCCESS" ? "paid" : "failed",
727
- paidAt: resource.success_time ? new Date(resource.success_time) : void 0,
728
- // 微信回调原始 payload 的强类型为 WechatNotifyResource,此处统一以 Record 暴露给上层做调试/审计
1192
+ currency: resource.amount.currency,
1193
+ status: "paid",
1194
+ paidAt: new Date(resource.success_time),
729
1195
  raw: resource
730
1196
  });
731
- } catch (cause) {
1197
+ } catch {
732
1198
  return err(
733
1199
  HaiPaymentError.NOTIFY_PARSE_FAILED,
734
1200
  paymentM("payment_notifyParseFailed"),
735
- cause
1201
+ new Error("Invalid Wechat payment notification")
736
1202
  );
737
1203
  }
738
1204
  },
@@ -822,6 +1288,7 @@ var payment = {
822
1288
  * 初始化支付模块
823
1289
  *
824
1290
  * 根据提供的配置自动注册对应的 Provider。
1291
+ * Stripe 要求共享 reldb 已初始化;仓库就绪后才发布 Provider,关闭时不代替宿主关闭 reldb。
825
1292
  *
826
1293
  * @param config - 支付配置
827
1294
  */
@@ -850,6 +1317,16 @@ var payment = {
850
1317
  );
851
1318
  }
852
1319
  const parsed = parseResult.data;
1320
+ let stripeProvider;
1321
+ if (parsed.stripe) {
1322
+ if (!reldb.isInitialized)
1323
+ return err(HaiPaymentError.CONFIG_ERROR, paymentM("payment_stripeDatabaseRequired"));
1324
+ const repository = new StripeOperationRepository(reldb);
1325
+ const ready = await repository.initialize();
1326
+ if (!ready.success)
1327
+ return err(HaiPaymentError.CONFIG_ERROR, paymentM("payment_stripeStoreInitFailed"), ready.error);
1328
+ stripeProvider = createStripeProvider(parsed.stripe, repository);
1329
+ }
853
1330
  clearProviders();
854
1331
  if (parsed.wechat) {
855
1332
  registerProvider(createWechatPayProvider(parsed.wechat));
@@ -857,9 +1334,8 @@ var payment = {
857
1334
  if (parsed.alipay) {
858
1335
  registerProvider(createAlipayProvider(parsed.alipay));
859
1336
  }
860
- if (parsed.stripe) {
861
- registerProvider(createStripeProvider(parsed.stripe));
862
- }
1337
+ if (stripeProvider)
1338
+ registerProvider(stripeProvider);
863
1339
  currentConfig = parsed;
864
1340
  logger2.info("Payment module initialized", {
865
1341
  providers: [