moltspay 2.0.0 → 2.4.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 (50) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +551 -0
  3. package/README.md +466 -8
  4. package/dist/cdp/index.js.map +1 -1
  5. package/dist/cdp/index.mjs.map +1 -1
  6. package/dist/chains/index.d.mts +69 -4
  7. package/dist/chains/index.d.ts +69 -4
  8. package/dist/chains/index.js +45 -1
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/index.mjs +39 -1
  11. package/dist/chains/index.mjs.map +1 -1
  12. package/dist/cli/index.js +5444 -2126
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/index.mjs +5457 -2133
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/client/index.d.mts +307 -1
  17. package/dist/client/index.d.ts +307 -1
  18. package/dist/client/index.js +639 -34
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +656 -52
  21. package/dist/client/index.mjs.map +1 -1
  22. package/dist/client/web/index.mjs.map +1 -1
  23. package/dist/facilitators/index.d.mts +512 -10
  24. package/dist/facilitators/index.d.ts +512 -10
  25. package/dist/facilitators/index.js +925 -13
  26. package/dist/facilitators/index.js.map +1 -1
  27. package/dist/facilitators/index.mjs +906 -12
  28. package/dist/facilitators/index.mjs.map +1 -1
  29. package/dist/index.d.mts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2843 -551
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +2849 -558
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/mcp/index.js +635 -32
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/mcp/index.mjs +660 -57
  38. package/dist/mcp/index.mjs.map +1 -1
  39. package/dist/server/index.d.mts +252 -11
  40. package/dist/server/index.d.ts +252 -11
  41. package/dist/server/index.js +2049 -261
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +2049 -261
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/verify/index.js.map +1 -1
  46. package/dist/verify/index.mjs.map +1 -1
  47. package/dist/wallet/index.js.map +1 -1
  48. package/dist/wallet/index.mjs.map +1 -1
  49. package/package.json +14 -2
  50. package/schemas/moltspay.services.schema.json +127 -16
@@ -23,22 +23,75 @@ interface ServiceConfig {
23
23
  /** Function name to import from skill's index.js (new skill-based approach) */
24
24
  function?: string;
25
25
  /**
26
- * Alipay AI per-service config (2.0.0+).
26
+ * Alipay AI Pay per-service config (2.0.0+).
27
27
  * Set when this service accepts CNY payments via the alipay rail.
28
28
  * The `price`/`currency` fields above still describe USDC pricing
29
29
  * for the x402 rails; `alipay.price_cny` is a separate CNY price.
30
30
  */
31
31
  alipay?: ServiceAlipayConfig;
32
+ /**
33
+ * WeChat Pay v3 Native per-service config (2.1.0+).
34
+ * Set when this service accepts CNY payments via the wechat rail.
35
+ * The `price`/`currency` fields above still describe USDC pricing for the
36
+ * x402 rails; `wechat.price_cny` is a separate CNY price.
37
+ */
38
+ wechat?: ServiceWechatConfig;
39
+ /**
40
+ * Custodial balance per-service config (2.2.0+).
41
+ * Set when this service accepts password-free deduction from
42
+ * server-custodied buyer balances. `balance.price` defaults to the
43
+ * service's top-level `price` when omitted.
44
+ */
45
+ balance?: ServiceBalanceConfig;
46
+ }
47
+ /**
48
+ * Per-service custodial balance configuration (2.2.0+).
49
+ *
50
+ * Sample:
51
+ * ```json
52
+ * "balance": {
53
+ * "price": "3.99"
54
+ * }
55
+ * ```
56
+ */
57
+ interface ServiceBalanceConfig {
58
+ /**
59
+ * Price in the ledger currency as a decimal string (e.g. `"3.99"`).
60
+ * Defaults to the service's top-level `price` when omitted.
61
+ * Must match `/^\d+(\.\d{1,2})?$/`.
62
+ */
63
+ price?: string;
64
+ }
65
+ /**
66
+ * Per-service WeChat Pay Native configuration (2.1.0+).
67
+ *
68
+ * Sample:
69
+ * ```json
70
+ * "wechat": {
71
+ * "price_cny": "10.00",
72
+ * "description": "Demo video - series one"
73
+ * }
74
+ * ```
75
+ */
76
+ interface ServiceWechatConfig {
77
+ /**
78
+ * CNY price as a decimal string in **yuan** (e.g. `"10.00"` = 10 CNY).
79
+ * NOT fen: `"100"` means 100 yuan, not 100 fen.
80
+ * Must match `/^\d+(\.\d{1,2})?$/`.
81
+ */
82
+ price_cny: string;
83
+ /** Order description shown to the payer in the WeChat app. */
84
+ description: string;
32
85
  }
33
86
  /**
34
- * Per-service Alipay AI configuration (2.0.0+).
87
+ * Per-service Alipay AI Pay configuration (2.0.0+).
35
88
  *
36
89
  * Sample:
37
90
  * ```json
38
91
  * "alipay": {
39
92
  * "service_id": "API_0EA6DC4FC99A4DF7",
40
93
  * "price_cny": "7.00",
41
- * "goods_name": "产品演示视频 - 系列一"
94
+ * "goods_name": "Product demo video - series 1"
42
95
  * }
43
96
  * ```
44
97
  */
@@ -46,8 +99,8 @@ interface ServiceAlipayConfig {
46
99
  /** Per-service service_id (defaults to provider.alipay.service_id_default). */
47
100
  service_id?: string;
48
101
  /**
49
- * CNY price as decimal string in **元** (e.g. `"7.00"` = 7 CNY).
50
- * NOT cents: `"100"` means 100 元, not 100 分.
102
+ * CNY price as decimal string in **yuan** (e.g. `"7.00"` = 7 CNY).
103
+ * NOT cents: `"100"` means 100 yuan, not 100 fen.
51
104
  * Must match `/^\d+(\.\d{1,2})?$/`.
52
105
  */
53
106
  price_cny: string;
@@ -77,15 +130,95 @@ interface ProviderConfig {
77
130
  chain?: string;
78
131
  chains?: ChainConfig[];
79
132
  /**
80
- * Alipay AI provider-level config (2.0.0+).
133
+ * Alipay AI Pay provider-level config (2.0.0+).
81
134
  * Required when `chains` includes `"alipay"`.
82
135
  * Server validates `private_key_path` / `alipay_public_key_path` are
83
136
  * readable + parse as RSA PEM at startup; rejects start otherwise.
84
137
  */
85
138
  alipay?: ProviderAlipayConfig;
139
+ /**
140
+ * WeChat Pay v3 provider-level config (2.1.0+).
141
+ * Required when `chains` includes `"wechat"`. The server resolves the PEM
142
+ * file paths + validates them at startup; rejects start otherwise.
143
+ */
144
+ wechat?: ProviderWechatConfig;
145
+ /**
146
+ * Custodial balance rail provider-level config (2.2.0+).
147
+ * Required when `chains` includes `"balance"`. Requires Node >= 22.5
148
+ * (node:sqlite); the server rejects start otherwise.
149
+ */
150
+ balance?: ProviderBalanceConfig;
86
151
  }
87
152
  /**
88
- * Provider-level Alipay AI configuration (2.0.0+).
153
+ * Provider-level custodial balance rail configuration (2.2.0+).
154
+ *
155
+ * Sample:
156
+ * ```json
157
+ * "balance": {
158
+ * "db_path": "./data/balance.sqlite",
159
+ * "currency": "USD",
160
+ * "single_limit": "5.00",
161
+ * "daily_limit": "10.00"
162
+ * }
163
+ * ```
164
+ */
165
+ interface ProviderBalanceConfig {
166
+ /** SQLite ledger file path (relative to moltspay.services.json). Created on first start. */
167
+ db_path: string;
168
+ /** Ledger quote currency. Defaults to `"USD"`. */
169
+ currency?: string;
170
+ /** Default per-transaction limit for new buyers, decimal string. Defaults to `"5.00"`. */
171
+ single_limit?: string;
172
+ /** Default daily limit for new buyers, decimal string. Defaults to `"10.00"`. */
173
+ daily_limit?: string;
174
+ /** Offered top-up pack amounts in ledger currency (2.3+), e.g. `["20.00","50.00"]`. */
175
+ topup_packs?: string[];
176
+ /** Pack the client auto-selects when a 402 finds an insufficient balance (2.3+). Must be one of `topup_packs`. */
177
+ default_pack?: string;
178
+ /** Ceiling on client auto-top-up without explicit pack selection (2.3+). */
179
+ auto_topup_max?: string;
180
+ /** User-auth rollout gate for deductions: `off` | `shadow` | `enforce`. Default `off`. */
181
+ auth_mode?: 'off' | 'shadow' | 'enforce';
182
+ }
183
+ /**
184
+ * Provider-level WeChat Pay v3 configuration (2.1.0+).
185
+ *
186
+ * The user-facing form uses FILE PATHS for the PEM keys; the server resolves
187
+ * them to PEM strings before constructing WechatFacilitator.
188
+ *
189
+ * Sample:
190
+ * ```json
191
+ * "wechat": {
192
+ * "mchid": "1900000001",
193
+ * "appid": "wx8888888888888888",
194
+ * "serial_no": "5157F09EFDC096DE15EBE81A47057A72...",
195
+ * "private_key_path": "./cert/wechat_apiclient_key.pem",
196
+ * "platform_public_key_path": "./cert/wechat_platform_cert.pem",
197
+ * "apiv3_key": "your32byteapiv3keyhere0123456789",
198
+ * "notify_url": "https://your.host/wechat/notify"
199
+ * }
200
+ * ```
201
+ */
202
+ interface ProviderWechatConfig {
203
+ /** Merchant id (mchid). */
204
+ mchid: string;
205
+ /** App id (official account / mini-program / app). */
206
+ appid: string;
207
+ /** Merchant API certificate serial number. */
208
+ serial_no: string;
209
+ /** Path to the merchant RSA private key PEM (relative to moltspay.services.json). */
210
+ private_key_path: string;
211
+ /** Path to the WeChat platform certificate public key PEM. Enables response verification. */
212
+ platform_public_key_path?: string;
213
+ /** APIv3 key (32 bytes). Only needed for callback decryption (Phase 2). */
214
+ apiv3_key?: string;
215
+ /** Async result notify URL. Required by Native order create even when polling. */
216
+ notify_url: string;
217
+ /** Open API base URL. Defaults to `"https://api.mch.weixin.qq.com"`. */
218
+ api_base?: string;
219
+ }
220
+ /**
221
+ * Provider-level Alipay AI Pay configuration (2.0.0+).
89
222
  *
90
223
  * The user-facing form uses FILE PATHS for both PEM keys; the server
91
224
  * resolves them to PEM strings before constructing AlipayFacilitator.
@@ -95,7 +228,7 @@ interface ProviderConfig {
95
228
  * "alipay": {
96
229
  * "seller_id": "2088641494699428",
97
230
  * "app_id": "2021006150642142",
98
- * "seller_name": "上海超响应数字科技有限公司",
231
+ * "seller_name": "Example Co., Ltd.",
99
232
  * "service_id_default": "API_0EA6DC4FC99A4DF7",
100
233
  * "private_key_path": "./cert/ALIPAY_PRIVATE_KEY.txt",
101
234
  * "alipay_public_key_path": "./cert/ALIPAY_PUBLIC_KEY.txt"
@@ -223,8 +356,29 @@ declare class MoltsPayServer {
223
356
  private registry;
224
357
  private networkId;
225
358
  private useMainnet;
226
- /** Alipay AI facilitator instance, set when `provider.alipay` is configured (2.0.0). */
359
+ /** Alipay AI Pay facilitator instance, set when `provider.alipay` is configured (2.0.0). */
227
360
  private alipayFacilitator;
361
+ /** WeChat Pay Native facilitator instance, set when `provider.wechat` is configured (2.1.0). */
362
+ private wechatFacilitator;
363
+ /** Custodial balance facilitator instance, set when `provider.balance` is configured (2.2.0). */
364
+ private balanceFacilitator;
365
+ private balanceEndpoints;
366
+ /**
367
+ * Pending WeChat Native order cache — the double-charge fix.
368
+ *
369
+ * Every `buildWechatChallenge` used to place a NEW Native order, so a client
370
+ * that received two 402s (e.g. initial challenge + one poll re-request that
371
+ * raced ahead of payment) could surface two live QRs and a buyer could pay
372
+ * both (confirmed ¥0.07×2 on 2026-07-02). Now the unpaid order is cached
373
+ * under a content-derived key — sha256(service id | canonical params |
374
+ * price_cny) — and reused until it is paid or its `time_expire` window
375
+ * nears expiry, so any number of 402 emits for the same purchase intent
376
+ * share ONE order, even across separate client processes.
377
+ * Storing the in-flight promise also dedupes concurrent 402 builds.
378
+ * In-memory by design: on restart the worst case is one extra unpaid order,
379
+ * which expires server-side per `time_expire` — never a double charge.
380
+ */
381
+ private wechatPendingChallenges;
228
382
  constructor(servicesPath: string, options?: MoltsPayServerOptionsExtended);
229
383
  /**
230
384
  * Register a skill handler for a service
@@ -266,6 +420,29 @@ declare class MoltsPayServer {
266
420
  * Handle incoming request
267
421
  */
268
422
  private handleRequest;
423
+ /**
424
+ * Public base URL prefix for self-describing links, from PUBLIC_BASE_URL
425
+ * (trailing slash stripped). Empty when unset, so emitted paths stay
426
+ * root-relative — behavior is unchanged for local / no-prefix deploys.
427
+ *
428
+ * Needed because nginx rewrites the deployment prefix (e.g.
429
+ * `/t/moltspay-server`) away before proxying, so the process cannot infer
430
+ * its own public prefix; a root-relative `/services` would otherwise
431
+ * resolve against the domain root and hit the wrong backend.
432
+ */
433
+ private get publicBase();
434
+ /**
435
+ * Per-service pricing across every configured rail, for the discovery
436
+ * payloads. The top-level `price`/`currency` (crypto/USDC) stay unchanged
437
+ * for back-compat; this surfaces the fiat + balance rails (CNY) that were
438
+ * previously invisible in discovery even though the manifest defines them
439
+ * and the 402 challenge already quotes them. `acceptedCurrencies` becomes
440
+ * the union across rails so a client can see CNY is accepted without
441
+ * first triggering a 402.
442
+ */
443
+ private describeServicePricing;
444
+ /** Shared service-list entry for the discovery and /services endpoints. */
445
+ private buildDiscoveryService;
269
446
  /**
270
447
  * GET /.well-known/agent-services.json - Standard discovery endpoint
271
448
  */
@@ -283,7 +460,7 @@ declare class MoltsPayServer {
283
460
  */
284
461
  private handleExecute;
285
462
  /**
286
- * Execute a service paid via the Alipay AI fiat rail (2.0.0).
463
+ * Execute a service paid via the Alipay AI Pay fiat rail (2.0.0).
287
464
  *
288
465
  * Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
289
466
  * validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
@@ -299,6 +476,70 @@ declare class MoltsPayServer {
299
476
  * 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
300
477
  */
301
478
  private buildAlipayChallenge;
479
+ /**
480
+ * Execute a service paid via the WeChat Pay v3 Native fiat rail (2.1.0).
481
+ *
482
+ * Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
483
+ * validation. The buyer (a human) scanned the Native QR and paid; the
484
+ * client re-requests carrying `out_trade_no` in the X-Payment payload.
485
+ * Verify queries the order (`trade_state === SUCCESS`). Settlement is an
486
+ * idempotent re-confirm and is FIRE-AND-FORGET (mirrors the Alipay path):
487
+ * a confirm failure is logged but does NOT fail the delivered response —
488
+ * the order was already verified SUCCESS.
489
+ */
490
+ private handleWechatExecute;
491
+ /**
492
+ * Build the WeChat 402 challenge for a service, or null when the wechat rail
493
+ * isn't configured for this server or this service. Placing a Native order
494
+ * is a network call that returns a fresh `code_url` + `out_trade_no`; the
495
+ * x402 `accepts[]` entry carries both in `extra` so the client can render
496
+ * the QR and later echo `out_trade_no` back for verification.
497
+ *
498
+ * DOUBLE-CHARGE FIX: the unpaid order is cached per service id (see
499
+ * `wechatPendingChallenges`), so repeated 402 emits within the order's
500
+ * `time_expire` window return the SAME `code_url`/`out_trade_no` instead of
501
+ * minting a new payable order each time. The entry is dropped once the
502
+ * order is paid (`invalidateWechatChallenge`) or shortly before it expires
503
+ * (refresh margin, so clients never receive a nearly-dead QR). A build
504
+ * failure is not cached and degrades gracefully (the other rails'
505
+ * accepts[] still ship).
506
+ */
507
+ private buildWechatChallenge;
508
+ /**
509
+ * Get-or-create a pending WeChat Native order under `cacheKey`, deduping
510
+ * concurrent builds and reusing an unpaid order until it nears expiry.
511
+ * Shared by the 402 challenge path ({@link buildWechatChallenge}) and the
512
+ * balance top-up order path ({@link handleBalanceTopupOrder}). See the
513
+ * `wechatPendingChallenges` doc for the double-charge rationale.
514
+ */
515
+ private getOrCreatePendingWechatOrder;
516
+ /**
517
+ * Drop the cached pending WeChat order that matches a paid `out_trade_no`,
518
+ * so the next 402 mints a fresh order instead of re-serving a consumed one
519
+ * (Native is one-code-one-payment).
520
+ */
521
+ private invalidateWechatChallenge;
522
+ /**
523
+ * Handle /execute for the custodial balance rail (2.2.0).
524
+ *
525
+ * Execution order is INVERTED relative to the QR rails: the deduction IS
526
+ * the settlement, so it must land before the skill runs, and a skill
527
+ * failure refunds it. `settle()` is idempotent on the client's
528
+ * `request_id`, so a retried request never double-charges.
529
+ *
530
+ * QR rails: verify(paid?) → run skill → settle (confirm, fire-and-forget)
531
+ * balance: verify implicit in settle (atomic deduct) → run skill → [fail → refund]
532
+ */
533
+ private handleBalanceExecute;
534
+ /** The balance rail's requirements for a service (price defaults to `config.price`). */
535
+ private balanceRequirementsFor;
536
+ /**
537
+ * Build the balance 402 challenge for a service, or null when the rail
538
+ * isn't configured for this server or this service. Pure — nothing is
539
+ * minted, so unlike the QR rails a 402 emit has no side effects.
540
+ */
541
+ private buildBalanceChallenge;
542
+ /** GET /balance?buyer_id= — balance, limits, and today's spend. */
302
543
  /**
303
544
  * Handle MPP (Machine Payments Protocol) request
304
545
  * Supports both x402 and MPP protocols on service endpoints
@@ -377,4 +618,4 @@ declare class MoltsPayServer {
377
618
  private sendProxyPaymentRequired;
378
619
  }
379
620
 
380
- export { type ChainConfig, type Charge, type ChargeStatus, type CorsOptions, type InputField, MoltsPayServer, type MoltsPayServerOptions, type MoltsPayServerOptionsExtended, type OutputField, type PaymentRequest, type ProviderAlipayConfig, type ProviderConfig, type RegisteredSkill, type ServiceAlipayConfig, type ServiceConfig, type ServicesManifest, type SkillFunction, type TokenSymbol, type VerifyRequest };
621
+ export { type ChainConfig, type Charge, type ChargeStatus, type CorsOptions, type InputField, MoltsPayServer, type MoltsPayServerOptions, type MoltsPayServerOptionsExtended, type OutputField, type PaymentRequest, type ProviderAlipayConfig, type ProviderBalanceConfig, type ProviderConfig, type ProviderWechatConfig, type RegisteredSkill, type ServiceAlipayConfig, type ServiceBalanceConfig, type ServiceConfig, type ServiceWechatConfig, type ServicesManifest, type SkillFunction, type TokenSymbol, type VerifyRequest };
@@ -23,22 +23,75 @@ interface ServiceConfig {
23
23
  /** Function name to import from skill's index.js (new skill-based approach) */
24
24
  function?: string;
25
25
  /**
26
- * Alipay AI per-service config (2.0.0+).
26
+ * Alipay AI Pay per-service config (2.0.0+).
27
27
  * Set when this service accepts CNY payments via the alipay rail.
28
28
  * The `price`/`currency` fields above still describe USDC pricing
29
29
  * for the x402 rails; `alipay.price_cny` is a separate CNY price.
30
30
  */
31
31
  alipay?: ServiceAlipayConfig;
32
+ /**
33
+ * WeChat Pay v3 Native per-service config (2.1.0+).
34
+ * Set when this service accepts CNY payments via the wechat rail.
35
+ * The `price`/`currency` fields above still describe USDC pricing for the
36
+ * x402 rails; `wechat.price_cny` is a separate CNY price.
37
+ */
38
+ wechat?: ServiceWechatConfig;
39
+ /**
40
+ * Custodial balance per-service config (2.2.0+).
41
+ * Set when this service accepts password-free deduction from
42
+ * server-custodied buyer balances. `balance.price` defaults to the
43
+ * service's top-level `price` when omitted.
44
+ */
45
+ balance?: ServiceBalanceConfig;
46
+ }
47
+ /**
48
+ * Per-service custodial balance configuration (2.2.0+).
49
+ *
50
+ * Sample:
51
+ * ```json
52
+ * "balance": {
53
+ * "price": "3.99"
54
+ * }
55
+ * ```
56
+ */
57
+ interface ServiceBalanceConfig {
58
+ /**
59
+ * Price in the ledger currency as a decimal string (e.g. `"3.99"`).
60
+ * Defaults to the service's top-level `price` when omitted.
61
+ * Must match `/^\d+(\.\d{1,2})?$/`.
62
+ */
63
+ price?: string;
64
+ }
65
+ /**
66
+ * Per-service WeChat Pay Native configuration (2.1.0+).
67
+ *
68
+ * Sample:
69
+ * ```json
70
+ * "wechat": {
71
+ * "price_cny": "10.00",
72
+ * "description": "Demo video - series one"
73
+ * }
74
+ * ```
75
+ */
76
+ interface ServiceWechatConfig {
77
+ /**
78
+ * CNY price as a decimal string in **yuan** (e.g. `"10.00"` = 10 CNY).
79
+ * NOT fen: `"100"` means 100 yuan, not 100 fen.
80
+ * Must match `/^\d+(\.\d{1,2})?$/`.
81
+ */
82
+ price_cny: string;
83
+ /** Order description shown to the payer in the WeChat app. */
84
+ description: string;
32
85
  }
33
86
  /**
34
- * Per-service Alipay AI configuration (2.0.0+).
87
+ * Per-service Alipay AI Pay configuration (2.0.0+).
35
88
  *
36
89
  * Sample:
37
90
  * ```json
38
91
  * "alipay": {
39
92
  * "service_id": "API_0EA6DC4FC99A4DF7",
40
93
  * "price_cny": "7.00",
41
- * "goods_name": "产品演示视频 - 系列一"
94
+ * "goods_name": "Product demo video - series 1"
42
95
  * }
43
96
  * ```
44
97
  */
@@ -46,8 +99,8 @@ interface ServiceAlipayConfig {
46
99
  /** Per-service service_id (defaults to provider.alipay.service_id_default). */
47
100
  service_id?: string;
48
101
  /**
49
- * CNY price as decimal string in **元** (e.g. `"7.00"` = 7 CNY).
50
- * NOT cents: `"100"` means 100 元, not 100 分.
102
+ * CNY price as decimal string in **yuan** (e.g. `"7.00"` = 7 CNY).
103
+ * NOT cents: `"100"` means 100 yuan, not 100 fen.
51
104
  * Must match `/^\d+(\.\d{1,2})?$/`.
52
105
  */
53
106
  price_cny: string;
@@ -77,15 +130,95 @@ interface ProviderConfig {
77
130
  chain?: string;
78
131
  chains?: ChainConfig[];
79
132
  /**
80
- * Alipay AI provider-level config (2.0.0+).
133
+ * Alipay AI Pay provider-level config (2.0.0+).
81
134
  * Required when `chains` includes `"alipay"`.
82
135
  * Server validates `private_key_path` / `alipay_public_key_path` are
83
136
  * readable + parse as RSA PEM at startup; rejects start otherwise.
84
137
  */
85
138
  alipay?: ProviderAlipayConfig;
139
+ /**
140
+ * WeChat Pay v3 provider-level config (2.1.0+).
141
+ * Required when `chains` includes `"wechat"`. The server resolves the PEM
142
+ * file paths + validates them at startup; rejects start otherwise.
143
+ */
144
+ wechat?: ProviderWechatConfig;
145
+ /**
146
+ * Custodial balance rail provider-level config (2.2.0+).
147
+ * Required when `chains` includes `"balance"`. Requires Node >= 22.5
148
+ * (node:sqlite); the server rejects start otherwise.
149
+ */
150
+ balance?: ProviderBalanceConfig;
86
151
  }
87
152
  /**
88
- * Provider-level Alipay AI configuration (2.0.0+).
153
+ * Provider-level custodial balance rail configuration (2.2.0+).
154
+ *
155
+ * Sample:
156
+ * ```json
157
+ * "balance": {
158
+ * "db_path": "./data/balance.sqlite",
159
+ * "currency": "USD",
160
+ * "single_limit": "5.00",
161
+ * "daily_limit": "10.00"
162
+ * }
163
+ * ```
164
+ */
165
+ interface ProviderBalanceConfig {
166
+ /** SQLite ledger file path (relative to moltspay.services.json). Created on first start. */
167
+ db_path: string;
168
+ /** Ledger quote currency. Defaults to `"USD"`. */
169
+ currency?: string;
170
+ /** Default per-transaction limit for new buyers, decimal string. Defaults to `"5.00"`. */
171
+ single_limit?: string;
172
+ /** Default daily limit for new buyers, decimal string. Defaults to `"10.00"`. */
173
+ daily_limit?: string;
174
+ /** Offered top-up pack amounts in ledger currency (2.3+), e.g. `["20.00","50.00"]`. */
175
+ topup_packs?: string[];
176
+ /** Pack the client auto-selects when a 402 finds an insufficient balance (2.3+). Must be one of `topup_packs`. */
177
+ default_pack?: string;
178
+ /** Ceiling on client auto-top-up without explicit pack selection (2.3+). */
179
+ auto_topup_max?: string;
180
+ /** User-auth rollout gate for deductions: `off` | `shadow` | `enforce`. Default `off`. */
181
+ auth_mode?: 'off' | 'shadow' | 'enforce';
182
+ }
183
+ /**
184
+ * Provider-level WeChat Pay v3 configuration (2.1.0+).
185
+ *
186
+ * The user-facing form uses FILE PATHS for the PEM keys; the server resolves
187
+ * them to PEM strings before constructing WechatFacilitator.
188
+ *
189
+ * Sample:
190
+ * ```json
191
+ * "wechat": {
192
+ * "mchid": "1900000001",
193
+ * "appid": "wx8888888888888888",
194
+ * "serial_no": "5157F09EFDC096DE15EBE81A47057A72...",
195
+ * "private_key_path": "./cert/wechat_apiclient_key.pem",
196
+ * "platform_public_key_path": "./cert/wechat_platform_cert.pem",
197
+ * "apiv3_key": "your32byteapiv3keyhere0123456789",
198
+ * "notify_url": "https://your.host/wechat/notify"
199
+ * }
200
+ * ```
201
+ */
202
+ interface ProviderWechatConfig {
203
+ /** Merchant id (mchid). */
204
+ mchid: string;
205
+ /** App id (official account / mini-program / app). */
206
+ appid: string;
207
+ /** Merchant API certificate serial number. */
208
+ serial_no: string;
209
+ /** Path to the merchant RSA private key PEM (relative to moltspay.services.json). */
210
+ private_key_path: string;
211
+ /** Path to the WeChat platform certificate public key PEM. Enables response verification. */
212
+ platform_public_key_path?: string;
213
+ /** APIv3 key (32 bytes). Only needed for callback decryption (Phase 2). */
214
+ apiv3_key?: string;
215
+ /** Async result notify URL. Required by Native order create even when polling. */
216
+ notify_url: string;
217
+ /** Open API base URL. Defaults to `"https://api.mch.weixin.qq.com"`. */
218
+ api_base?: string;
219
+ }
220
+ /**
221
+ * Provider-level Alipay AI Pay configuration (2.0.0+).
89
222
  *
90
223
  * The user-facing form uses FILE PATHS for both PEM keys; the server
91
224
  * resolves them to PEM strings before constructing AlipayFacilitator.
@@ -95,7 +228,7 @@ interface ProviderConfig {
95
228
  * "alipay": {
96
229
  * "seller_id": "2088641494699428",
97
230
  * "app_id": "2021006150642142",
98
- * "seller_name": "上海超响应数字科技有限公司",
231
+ * "seller_name": "Example Co., Ltd.",
99
232
  * "service_id_default": "API_0EA6DC4FC99A4DF7",
100
233
  * "private_key_path": "./cert/ALIPAY_PRIVATE_KEY.txt",
101
234
  * "alipay_public_key_path": "./cert/ALIPAY_PUBLIC_KEY.txt"
@@ -223,8 +356,29 @@ declare class MoltsPayServer {
223
356
  private registry;
224
357
  private networkId;
225
358
  private useMainnet;
226
- /** Alipay AI facilitator instance, set when `provider.alipay` is configured (2.0.0). */
359
+ /** Alipay AI Pay facilitator instance, set when `provider.alipay` is configured (2.0.0). */
227
360
  private alipayFacilitator;
361
+ /** WeChat Pay Native facilitator instance, set when `provider.wechat` is configured (2.1.0). */
362
+ private wechatFacilitator;
363
+ /** Custodial balance facilitator instance, set when `provider.balance` is configured (2.2.0). */
364
+ private balanceFacilitator;
365
+ private balanceEndpoints;
366
+ /**
367
+ * Pending WeChat Native order cache — the double-charge fix.
368
+ *
369
+ * Every `buildWechatChallenge` used to place a NEW Native order, so a client
370
+ * that received two 402s (e.g. initial challenge + one poll re-request that
371
+ * raced ahead of payment) could surface two live QRs and a buyer could pay
372
+ * both (confirmed ¥0.07×2 on 2026-07-02). Now the unpaid order is cached
373
+ * under a content-derived key — sha256(service id | canonical params |
374
+ * price_cny) — and reused until it is paid or its `time_expire` window
375
+ * nears expiry, so any number of 402 emits for the same purchase intent
376
+ * share ONE order, even across separate client processes.
377
+ * Storing the in-flight promise also dedupes concurrent 402 builds.
378
+ * In-memory by design: on restart the worst case is one extra unpaid order,
379
+ * which expires server-side per `time_expire` — never a double charge.
380
+ */
381
+ private wechatPendingChallenges;
228
382
  constructor(servicesPath: string, options?: MoltsPayServerOptionsExtended);
229
383
  /**
230
384
  * Register a skill handler for a service
@@ -266,6 +420,29 @@ declare class MoltsPayServer {
266
420
  * Handle incoming request
267
421
  */
268
422
  private handleRequest;
423
+ /**
424
+ * Public base URL prefix for self-describing links, from PUBLIC_BASE_URL
425
+ * (trailing slash stripped). Empty when unset, so emitted paths stay
426
+ * root-relative — behavior is unchanged for local / no-prefix deploys.
427
+ *
428
+ * Needed because nginx rewrites the deployment prefix (e.g.
429
+ * `/t/moltspay-server`) away before proxying, so the process cannot infer
430
+ * its own public prefix; a root-relative `/services` would otherwise
431
+ * resolve against the domain root and hit the wrong backend.
432
+ */
433
+ private get publicBase();
434
+ /**
435
+ * Per-service pricing across every configured rail, for the discovery
436
+ * payloads. The top-level `price`/`currency` (crypto/USDC) stay unchanged
437
+ * for back-compat; this surfaces the fiat + balance rails (CNY) that were
438
+ * previously invisible in discovery even though the manifest defines them
439
+ * and the 402 challenge already quotes them. `acceptedCurrencies` becomes
440
+ * the union across rails so a client can see CNY is accepted without
441
+ * first triggering a 402.
442
+ */
443
+ private describeServicePricing;
444
+ /** Shared service-list entry for the discovery and /services endpoints. */
445
+ private buildDiscoveryService;
269
446
  /**
270
447
  * GET /.well-known/agent-services.json - Standard discovery endpoint
271
448
  */
@@ -283,7 +460,7 @@ declare class MoltsPayServer {
283
460
  */
284
461
  private handleExecute;
285
462
  /**
286
- * Execute a service paid via the Alipay AI fiat rail (2.0.0).
463
+ * Execute a service paid via the Alipay AI Pay fiat rail (2.0.0).
287
464
  *
288
465
  * Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
289
466
  * validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
@@ -299,6 +476,70 @@ declare class MoltsPayServer {
299
476
  * 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
300
477
  */
301
478
  private buildAlipayChallenge;
479
+ /**
480
+ * Execute a service paid via the WeChat Pay v3 Native fiat rail (2.1.0).
481
+ *
482
+ * Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
483
+ * validation. The buyer (a human) scanned the Native QR and paid; the
484
+ * client re-requests carrying `out_trade_no` in the X-Payment payload.
485
+ * Verify queries the order (`trade_state === SUCCESS`). Settlement is an
486
+ * idempotent re-confirm and is FIRE-AND-FORGET (mirrors the Alipay path):
487
+ * a confirm failure is logged but does NOT fail the delivered response —
488
+ * the order was already verified SUCCESS.
489
+ */
490
+ private handleWechatExecute;
491
+ /**
492
+ * Build the WeChat 402 challenge for a service, or null when the wechat rail
493
+ * isn't configured for this server or this service. Placing a Native order
494
+ * is a network call that returns a fresh `code_url` + `out_trade_no`; the
495
+ * x402 `accepts[]` entry carries both in `extra` so the client can render
496
+ * the QR and later echo `out_trade_no` back for verification.
497
+ *
498
+ * DOUBLE-CHARGE FIX: the unpaid order is cached per service id (see
499
+ * `wechatPendingChallenges`), so repeated 402 emits within the order's
500
+ * `time_expire` window return the SAME `code_url`/`out_trade_no` instead of
501
+ * minting a new payable order each time. The entry is dropped once the
502
+ * order is paid (`invalidateWechatChallenge`) or shortly before it expires
503
+ * (refresh margin, so clients never receive a nearly-dead QR). A build
504
+ * failure is not cached and degrades gracefully (the other rails'
505
+ * accepts[] still ship).
506
+ */
507
+ private buildWechatChallenge;
508
+ /**
509
+ * Get-or-create a pending WeChat Native order under `cacheKey`, deduping
510
+ * concurrent builds and reusing an unpaid order until it nears expiry.
511
+ * Shared by the 402 challenge path ({@link buildWechatChallenge}) and the
512
+ * balance top-up order path ({@link handleBalanceTopupOrder}). See the
513
+ * `wechatPendingChallenges` doc for the double-charge rationale.
514
+ */
515
+ private getOrCreatePendingWechatOrder;
516
+ /**
517
+ * Drop the cached pending WeChat order that matches a paid `out_trade_no`,
518
+ * so the next 402 mints a fresh order instead of re-serving a consumed one
519
+ * (Native is one-code-one-payment).
520
+ */
521
+ private invalidateWechatChallenge;
522
+ /**
523
+ * Handle /execute for the custodial balance rail (2.2.0).
524
+ *
525
+ * Execution order is INVERTED relative to the QR rails: the deduction IS
526
+ * the settlement, so it must land before the skill runs, and a skill
527
+ * failure refunds it. `settle()` is idempotent on the client's
528
+ * `request_id`, so a retried request never double-charges.
529
+ *
530
+ * QR rails: verify(paid?) → run skill → settle (confirm, fire-and-forget)
531
+ * balance: verify implicit in settle (atomic deduct) → run skill → [fail → refund]
532
+ */
533
+ private handleBalanceExecute;
534
+ /** The balance rail's requirements for a service (price defaults to `config.price`). */
535
+ private balanceRequirementsFor;
536
+ /**
537
+ * Build the balance 402 challenge for a service, or null when the rail
538
+ * isn't configured for this server or this service. Pure — nothing is
539
+ * minted, so unlike the QR rails a 402 emit has no side effects.
540
+ */
541
+ private buildBalanceChallenge;
542
+ /** GET /balance?buyer_id= — balance, limits, and today's spend. */
302
543
  /**
303
544
  * Handle MPP (Machine Payments Protocol) request
304
545
  * Supports both x402 and MPP protocols on service endpoints
@@ -377,4 +618,4 @@ declare class MoltsPayServer {
377
618
  private sendProxyPaymentRequired;
378
619
  }
379
620
 
380
- export { type ChainConfig, type Charge, type ChargeStatus, type CorsOptions, type InputField, MoltsPayServer, type MoltsPayServerOptions, type MoltsPayServerOptionsExtended, type OutputField, type PaymentRequest, type ProviderAlipayConfig, type ProviderConfig, type RegisteredSkill, type ServiceAlipayConfig, type ServiceConfig, type ServicesManifest, type SkillFunction, type TokenSymbol, type VerifyRequest };
621
+ export { type ChainConfig, type Charge, type ChargeStatus, type CorsOptions, type InputField, MoltsPayServer, type MoltsPayServerOptions, type MoltsPayServerOptionsExtended, type OutputField, type PaymentRequest, type ProviderAlipayConfig, type ProviderBalanceConfig, type ProviderConfig, type ProviderWechatConfig, type RegisteredSkill, type ServiceAlipayConfig, type ServiceBalanceConfig, type ServiceConfig, type ServiceWechatConfig, type ServicesManifest, type SkillFunction, type TokenSymbol, type VerifyRequest };