moltspay 1.6.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 (57) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +551 -0
  3. package/README.md +466 -7
  4. package/dist/cdp/index.js.map +1 -1
  5. package/dist/cdp/index.mjs.map +1 -1
  6. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  7. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  8. package/dist/chains/index.d.mts +102 -1
  9. package/dist/chains/index.d.ts +102 -1
  10. package/dist/chains/index.js +66 -0
  11. package/dist/chains/index.js.map +1 -1
  12. package/dist/chains/index.mjs +57 -0
  13. package/dist/chains/index.mjs.map +1 -1
  14. package/dist/cli/index.js +6850 -2127
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/index.mjs +6857 -2128
  17. package/dist/cli/index.mjs.map +1 -1
  18. package/dist/client/index.d.mts +348 -1
  19. package/dist/client/index.d.ts +348 -1
  20. package/dist/client/index.js +1462 -43
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +1469 -51
  23. package/dist/client/index.mjs.map +1 -1
  24. package/dist/client/web/index.d.mts +17 -1
  25. package/dist/client/web/index.mjs +11 -0
  26. package/dist/client/web/index.mjs.map +1 -1
  27. package/dist/facilitators/index.d.mts +663 -4
  28. package/dist/facilitators/index.d.ts +663 -4
  29. package/dist/facilitators/index.js +1292 -10
  30. package/dist/facilitators/index.js.map +1 -1
  31. package/dist/facilitators/index.mjs +1268 -9
  32. package/dist/facilitators/index.mjs.map +1 -1
  33. package/dist/index.d.mts +78 -3
  34. package/dist/index.d.ts +78 -3
  35. package/dist/index.js +4229 -556
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +4214 -548
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/mcp/index.js +1462 -45
  40. package/dist/mcp/index.js.map +1 -1
  41. package/dist/mcp/index.mjs +1473 -56
  42. package/dist/mcp/index.mjs.map +1 -1
  43. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  44. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  45. package/dist/server/index.d.mts +336 -2
  46. package/dist/server/index.d.ts +336 -2
  47. package/dist/server/index.js +2516 -188
  48. package/dist/server/index.js.map +1 -1
  49. package/dist/server/index.mjs +2516 -188
  50. package/dist/server/index.mjs.map +1 -1
  51. package/dist/verify/index.js.map +1 -1
  52. package/dist/verify/index.mjs.map +1 -1
  53. package/dist/wallet/index.js.map +1 -1
  54. package/dist/wallet/index.mjs.map +1 -1
  55. package/package.json +20 -2
  56. package/schemas/moltspay.services.schema.json +211 -6
  57. package/scripts/postinstall.js +91 -0
@@ -1,6 +1,6 @@
1
- import { B as BaseFacilitator, H as HealthCheckResult, X as X402PaymentPayload, f as X402PaymentRequirements, V as VerifyResult, e as SettleResult } from '../registry-OsEO2dOu.mjs';
2
- export { F as Facilitator, a as FacilitatorConfig, b as FacilitatorFee, c as FacilitatorRegistry, d as FacilitatorSelection, S as SelectionStrategy, g as createRegistry, h as getDefaultRegistry } from '../registry-OsEO2dOu.mjs';
3
- export { C as CDPFacilitator, a as CDPFacilitatorConfig } from '../cdp-p_eHuQpb.mjs';
1
+ import { B as BaseFacilitator, H as HealthCheckResult, X as X402PaymentPayload, a as X402PaymentRequirements, V as VerifyResult, S as SettleResult } from '../registry-DIo0WNoO.mjs';
2
+ export { c as Facilitator, F as FacilitatorConfig, b as FacilitatorFee, d as FacilitatorRegistry, e as FacilitatorSelection, f as SelectionStrategy, g as createRegistry, h as getDefaultRegistry } from '../registry-DIo0WNoO.mjs';
3
+ export { C as CDPFacilitator, a as CDPFacilitatorConfig } from '../cdp-D-5Hg3y_.mjs';
4
4
  import { Keypair, PublicKey, Connection, Transaction } from '@solana/web3.js';
5
5
 
6
6
  /**
@@ -267,4 +267,663 @@ declare class SolanaFacilitator extends BaseFacilitator {
267
267
  */
268
268
  declare function createSolanaPaymentTransaction(senderPubkey: PublicKey, recipientPubkey: PublicKey, amount: bigint, chain: SolanaChainName, feePayerPubkey?: PublicKey, connection?: Connection): Promise<Transaction>;
269
269
 
270
- export { BNBFacilitator, type BNBPaymentIntent, BaseFacilitator, HealthCheckResult, SettleResult, SolanaFacilitator, type SolanaPaymentPayload, TempoFacilitator, VerifyResult, X402PaymentPayload, X402PaymentRequirements, createIntentTypedData, createSolanaPaymentTransaction };
270
+ /**
271
+ * Alipay AI Pay Facilitator (2.0.0)
272
+ *
273
+ * Implements the `Facilitator` interface for Alipay's AI Pay 402
274
+ * protocol. Adds a fiat rail (CNY) alongside the existing USDC/EVM/SVM
275
+ * rails. Server-side only; clients shell out to the `alipay-bot` CLI
276
+ * (see `AlipayClient` under `src/client/alipay/` in 1.7.0-rc.2).
277
+ *
278
+ * Key protocol differences from x402:
279
+ * - Wire challenge is Base64URL-encoded `Payment-Needed` header with
280
+ * nested `{protocol, method}` JSON (not flat `accepts[]`).
281
+ * - Amount unit is **yuan** (CNY decimal string, not atomic units).
282
+ * - Signature is RSA2 (SHA256WithRSA), not EIP-712 / EIP-3009.
283
+ * - Verify/settle hit Alipay Open API HTTP endpoints, not chain RPC.
284
+ *
285
+ * The server emits **both** `X-Payment-Required` and `Payment-Needed`
286
+ * headers so that legacy `alipay-bot` skills (`@alipay/agent-payment@1.0.9`)
287
+ * keep working without changes.
288
+ *
289
+ * @see ../../docs/ALIPAY-RAIL.md — end-user integration guide
290
+ * @see ../../docs/ALIPAY-INTEGRATION-DESIGN.md — architecture & decisions
291
+ * @see ../../docs/ALIPAY-INTEGRATION-PLAN.md — implementation milestones
292
+ *
293
+ * Stub for 1.7.0-rc.1; bodies tracked in ALIPAY-INTEGRATION-PLAN.md §1.
294
+ */
295
+
296
+ /** Network identifier exposed via `Facilitator.supportedNetworks`. */
297
+ declare const ALIPAY_NETWORK = "alipay";
298
+ /** x402 `scheme` string identifying the Alipay rail in `accepts[]`. */
299
+ declare const ALIPAY_SCHEME = "alipay-aipay";
300
+ /** Default production gateway URL. */
301
+ declare const ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
302
+ /** Sandbox gateway URL (for testing without real CNY). */
303
+ declare const ALIPAY_GATEWAY_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
304
+ /**
305
+ * Facilitator-level configuration sourced from `provider.alipay` in
306
+ * `moltspay.services.json`. The server resolves `private_key_path` and
307
+ * `alipay_public_key_path` to PEM strings before constructing the facilitator.
308
+ */
309
+ interface AlipayFacilitatorConfig {
310
+ /** Merchant Alipay ID (16 digits, e.g. `"2088641494699428"`). */
311
+ seller_id: string;
312
+ /** Application ID from Alipay Open Platform. */
313
+ app_id: string;
314
+ /** Merchant legal name; appears in `method.seller_name` of the 402 challenge. */
315
+ seller_name: string;
316
+ /** Fallback `service_id` when a service doesn't override it. */
317
+ service_id_default: string;
318
+ /** RSA2 merchant private key (PEM). Loaded from `private_key_path` by the server. */
319
+ private_key_pem: string;
320
+ /** Alipay platform public key (PEM). Loaded from `alipay_public_key_path` by the server. */
321
+ alipay_public_key_pem: string;
322
+ /** Open API gateway URL. Defaults to {@link ALIPAY_GATEWAY_PROD}. */
323
+ gateway_url?: string;
324
+ /** Signature algorithm. Only `RSA2` is supported. */
325
+ sign_type?: 'RSA2';
326
+ }
327
+ /**
328
+ * Inputs required to construct a 402 `Payment-Needed` challenge for a service.
329
+ */
330
+ interface CreatePaymentRequirementsOpts$1 {
331
+ /** Per-service Alipay `service_id` (overrides `provider.alipay.service_id_default`). */
332
+ serviceId: string;
333
+ /** CNY price as decimal string in **yuan** (e.g. `"1.00"` = 1 CNY). */
334
+ priceCny: string;
335
+ /** Goods name shown to the user in the Alipay app. */
336
+ goodsName: string;
337
+ /** Resource identifier (typically the request URL or its hash). */
338
+ resourceId: string;
339
+ /** Client-supplied `out_trade_no`; if omitted the facilitator generates one. */
340
+ outTradeNo?: string;
341
+ }
342
+ /**
343
+ * Both headers the server emits in a 402 response:
344
+ * - `x402Accepts` → `X-Payment-Required` (x402 standard, for new MoltsPay clients)
345
+ * - `paymentNeededHeader` → `Payment-Needed` (Alipay standard, for legacy `alipay-bot`)
346
+ *
347
+ * The two headers are mirrors of each other; the single source of truth lives
348
+ * in the server config.
349
+ */
350
+ interface AlipayPaymentRequirements {
351
+ x402Accepts: X402PaymentRequirements;
352
+ paymentNeededHeader: string;
353
+ }
354
+ /**
355
+ * Decoded `Payment-Proof` header from the buyer's request.
356
+ *
357
+ * The proof is Base64URL of `{protocol: {...}, method: {...}}` where:
358
+ * - `protocol.trade_no` is the 32-digit Alipay trade number
359
+ * - `protocol.payment_proof` is the RSA2-signed payment proof
360
+ * - `method.client_session` echoes the buyer's session for replay protection
361
+ */
362
+ interface AlipayPaymentProof {
363
+ protocol: {
364
+ payment_proof: string;
365
+ trade_no: string;
366
+ [k: string]: unknown;
367
+ };
368
+ method: {
369
+ client_session: string;
370
+ [k: string]: unknown;
371
+ };
372
+ }
373
+ /**
374
+ * Alipay AI Pay facilitator.
375
+ *
376
+ * Construction is cheap; expensive setup (key parsing, gateway probe) is
377
+ * deferred to `healthCheck()`.
378
+ */
379
+ declare class AlipayFacilitator extends BaseFacilitator {
380
+ readonly name = "alipay";
381
+ readonly displayName = "Alipay AI Pay";
382
+ readonly supportedNetworks: string[];
383
+ private readonly config;
384
+ constructor(config: AlipayFacilitatorConfig);
385
+ /**
386
+ * Build the 402 challenge for a service: signs the 8-field payload with
387
+ * RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
388
+ * `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
389
+ */
390
+ createPaymentRequirements(opts: CreatePaymentRequirementsOpts$1): Promise<AlipayPaymentRequirements>;
391
+ /**
392
+ * Verify a `Payment-Proof` by calling
393
+ * `alipay.aipay.agent.payment.verify` against the Alipay Open API.
394
+ *
395
+ * The proof is conveyed via `paymentPayload.payload`, which may be:
396
+ * - a raw Base64URL string (the `Payment-Proof` header value), or
397
+ * - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
398
+ *
399
+ * All failure modes (malformed payload, network errors, Alipay
400
+ * `code != 10000`) return `{ valid: false, error }`. No exception
401
+ * escapes, regardless of client-supplied input.
402
+ */
403
+ verify(paymentPayload: X402PaymentPayload, _requirements: X402PaymentRequirements): Promise<VerifyResult>;
404
+ /**
405
+ * Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
406
+ * service resource has been returned to the buyer.
407
+ *
408
+ * Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
409
+ * (a fulfillment-confirm failure), this is **fire-and-forget**: the caller (registry /
410
+ * server) is expected to log fulfillment failures but NOT roll back
411
+ * the already-delivered resource.
412
+ *
413
+ * Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
414
+ * does the same; the redundant Base64URL decode is negligible).
415
+ */
416
+ settle(paymentPayload: X402PaymentPayload, _requirements: X402PaymentRequirements): Promise<SettleResult>;
417
+ /**
418
+ * Validate that the configured RSA keys parse and that the Open API
419
+ * gateway is reachable. Does NOT make a real business API call (would
420
+ * burn quota and could appear as a legitimate verify attempt in logs).
421
+ */
422
+ healthCheck(): Promise<HealthCheckResult>;
423
+ /** Bundle the facilitator config into the shape openapi.ts wants. */
424
+ private getOpenApiConfig;
425
+ }
426
+
427
+ /**
428
+ * WeChat Pay v3 JSON API caller.
429
+ *
430
+ * Wraps the REST/JSON gateway (`https://api.mch.weixin.qq.com`). Handles:
431
+ * - SHA256-RSA request signing + the `Authorization` header (see ./sign.ts)
432
+ * - JSON body / response marshalling
433
+ * - optional response signature verification against the WeChat platform
434
+ * public key (enabled when `platform_public_key_pem` is configured)
435
+ * - uniform error surfacing: non-2xx throws a {@link WechatApiError} carrying
436
+ * the gateway `code`/`message`
437
+ *
438
+ * Unlike Alipay's form-urlencoded gateway, v3 is plain REST: the method +
439
+ * path + body are signed, and the business payload IS the HTTP body (no
440
+ * `biz_content` wrapper).
441
+ *
442
+ * @see https://pay.weixin.qq.com/docs/merchant/development/interface-rules/
443
+ */
444
+ /** Default production base URL for the v3 REST gateway. */
445
+ declare const WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
446
+
447
+ /**
448
+ * WeChat Pay v3 Facilitator (Native, scenario A).
449
+ *
450
+ * Implements the `Facilitator` interface for WeChat Pay's v3 **Native**
451
+ * (scan-to-pay) flow. Adds a CNY fiat rail alongside the USDC/EVM/SVM and
452
+ * Alipay rails. Server-side only.
453
+ *
454
+ * Scenario A — "agent issues a code, payer is not pre-bound":
455
+ * - `createPaymentRequirements` places a Native order and returns its
456
+ * `code_url`. The code is **payer-agnostic** (no openid, unlike JSAPI):
457
+ * any WeChat user can scan it. It is **one-code-one-payment** — the first
458
+ * payer settles the order; collect again by issuing a new code.
459
+ * - `verify` polls the order (`trade_state === 'SUCCESS'`).
460
+ * - `settle` re-confirms SUCCESS and returns the `transaction_id` (Native
461
+ * captures funds at SUCCESS; there is no separate capture step).
462
+ *
463
+ * Key protocol differences from Alipay AI Pay (see docs/WECHAT-RAIL-DESIGN.md §3):
464
+ * - REST/JSON gateway, not a form-urlencoded `gateway.do`.
465
+ * - SHA256-RSA over `METHOD\nURL\nTS\nNONCE\nBODY\n`, packed into the
466
+ * `Authorization` header (handled by ./wechat/api.ts + ./wechat/sign.ts).
467
+ * - Amount unit is **fen** (integer cents), not yuan — `cnyToFen` converts.
468
+ *
469
+ * Async callback decryption (AES-256-GCM) and the notify webhook are NOT in
470
+ * this milestone; scenario A confirms via polling. They land in Phase 2.
471
+ *
472
+ * @see ./wechat/sign.ts — signing primitives
473
+ * @see ./wechat/api.ts — v3 JSON caller
474
+ * @see ../../docs/WECHAT-RAIL-DESIGN.md — design & scenario
475
+ */
476
+
477
+ /** Network identifier exposed via `Facilitator.supportedNetworks`. */
478
+ declare const WECHAT_NETWORK = "wechat";
479
+ /** x402 `scheme` string identifying the WeChat Native rail in `accepts[]`. */
480
+ declare const WECHAT_SCHEME = "wechatpay-native";
481
+ /** Validation regex for `price_cny` (decimal string, unit yuan, <= 2 decimals). */
482
+ declare const WECHAT_AMOUNT_REGEX: RegExp;
483
+ /** Default order lifetime if the caller doesn't pass one (Native convention). */
484
+ declare const WECHAT_TIME_EXPIRE_MS: number;
485
+
486
+ /**
487
+ * Facilitator-level config sourced from `provider.wechat` in
488
+ * `moltspay.services.json`. The server resolves `private_key_path` /
489
+ * `platform_public_key_path` to PEM strings before constructing this.
490
+ */
491
+ interface WechatFacilitatorConfig {
492
+ /** Merchant id (mchid). */
493
+ mchid: string;
494
+ /** App id (official account / mini-program / app). */
495
+ appid: string;
496
+ /** Merchant API certificate serial number. */
497
+ serial_no: string;
498
+ /** Merchant RSA private key (PEM). */
499
+ private_key_pem: string;
500
+ /** WeChat platform certificate public key (PEM). Enables response verify. */
501
+ platform_public_key_pem?: string;
502
+ /** APIv3 key (32 bytes). Only needed for callback decryption (Phase 2). */
503
+ apiv3_key?: string;
504
+ /** Async result notify URL. **Required by Native order create** even when polling. */
505
+ notify_url: string;
506
+ /** Base URL; defaults to {@link WECHAT_API_BASE}. */
507
+ api_base?: string;
508
+ }
509
+ /** Inputs to place a Native order (402 challenge) for a service. */
510
+ interface CreatePaymentRequirementsOpts {
511
+ /** CNY price as a decimal string in **yuan** (e.g. `"10.00"`). */
512
+ priceCny: string;
513
+ /** Order description shown to the payer in the WeChat app. */
514
+ description: string;
515
+ /** Client-supplied `out_trade_no`; generated when omitted. */
516
+ outTradeNo?: string;
517
+ /** Order lifetime; defaults to {@link WECHAT_TIME_EXPIRE_MS}. */
518
+ expiresInMs?: number;
519
+ /**
520
+ * Passthrough metadata WeChat echoes back on order-query and callback
521
+ * (v3 `attach`, max 128 bytes once JSON-serialized). Used to bind an
522
+ * otherwise payer-agnostic Native order to a `buyer_id` for balance
523
+ * top-ups. Read it back with {@link parseWechatAttach}.
524
+ */
525
+ attach?: Record<string, string>;
526
+ }
527
+ /** Result of placing a Native order. */
528
+ interface WechatPaymentRequirements {
529
+ /** x402 `accepts[]` entry (carries `extra.code_url` + `extra.out_trade_no`). */
530
+ x402Accepts: X402PaymentRequirements;
531
+ /** `weixin://wxpay/bizpayurl?pr=...` — render verbatim as a QR. */
532
+ codeUrl: string;
533
+ /** The order's merchant trade number, used to poll `verify`/`settle`. */
534
+ outTradeNo: string;
535
+ }
536
+ /**
537
+ * WeChat Pay v3 Native facilitator.
538
+ *
539
+ * Construction is cheap; key parsing / gateway probe is deferred to
540
+ * `healthCheck()`.
541
+ */
542
+ declare class WechatFacilitator extends BaseFacilitator {
543
+ readonly name = "wechat";
544
+ readonly displayName = "WeChat Pay";
545
+ readonly supportedNetworks: string[];
546
+ private readonly config;
547
+ constructor(config: WechatFacilitatorConfig);
548
+ /**
549
+ * Place a Native order and build the 402 challenge. The returned
550
+ * `code_url` is payer-agnostic — any WeChat user may scan it.
551
+ */
552
+ createPaymentRequirements(opts: CreatePaymentRequirementsOpts): Promise<WechatPaymentRequirements>;
553
+ /**
554
+ * Poll an order: `trade_state === 'SUCCESS'` ⇒ paid. All failure modes
555
+ * (missing out_trade_no, gateway error, not-yet-paid) return
556
+ * `{ valid: false, error }`; no exception escapes.
557
+ */
558
+ verify(paymentPayload: X402PaymentPayload, requirements: X402PaymentRequirements): Promise<VerifyResult>;
559
+ /**
560
+ * Confirm settlement. Native captures funds at SUCCESS, so this is an
561
+ * idempotent re-confirm that returns the `transaction_id`. Like Alipay's
562
+ * fulfillment confirm, failures are surfaced but non-fatal to an
563
+ * already-delivered resource (caller logs, does not roll back).
564
+ */
565
+ settle(paymentPayload: X402PaymentPayload, requirements: X402PaymentRequirements): Promise<SettleResult>;
566
+ /**
567
+ * Validate keys parse, apiv3 key length, and gateway reachability. Does
568
+ * NOT make a business API call.
569
+ */
570
+ healthCheck(): Promise<HealthCheckResult>;
571
+ /** Query a Native order by out_trade_no. The query string is part of the signed path. */
572
+ private queryOrder;
573
+ /** Project the facilitator config down to what api.ts needs. */
574
+ private getApiConfig;
575
+ }
576
+ /**
577
+ * Safely parse the `attach` string echoed back on order-query / callback into
578
+ * the object passed to {@link CreatePaymentRequirementsOpts.attach}. Returns
579
+ * null for a missing, non-string, or malformed value (a per-transaction order
580
+ * that carried no attach, or tampered input) so callers never throw on it.
581
+ */
582
+ declare function parseWechatAttach(attach: unknown): Record<string, string> | null;
583
+
584
+ /**
585
+ * Custodial balance ledger (SQLite).
586
+ *
587
+ * Backs the balance ("password-free" / password-free) rail: buyers top up once and
588
+ * subsequent purchases are deducted server-side — no signature or scan per
589
+ * transaction. All amounts are **integer cents** (`*_sat`, 1 USD = 100) to
590
+ * avoid floating-point drift; the dollar<->cent boundary is `toSat`/`fromSat`.
591
+ *
592
+ * Uses the Node built-in `node:sqlite` (zero new dependencies). That module
593
+ * is only available on Node >= 22.5, so it is loaded lazily inside the
594
+ * constructor: servers that don't enable the balance rail keep the package's
595
+ * `node >= 18` floor.
596
+ *
597
+ * Concurrency: `DatabaseSync` is synchronous and single-connection, so every
598
+ * ledger call is naturally serialized within the process — the
599
+ * check-and-deduct UPDATE (`... WHERE balance_sat >= ?`) is atomic without
600
+ * explicit transactions. WAL mode keeps concurrent readers cheap.
601
+ *
602
+ * @see ../../../docs/BALANCE-RAIL-DESIGN.md — data model & semantics
603
+ */
604
+ /** Buyer account row (amounts in integer cents). */
605
+ interface BuyerRow {
606
+ buyer_id: string;
607
+ display_name: string | null;
608
+ balance_sat: number;
609
+ total_topup_sat: number;
610
+ total_spent_sat: number;
611
+ daily_limit_sat: number;
612
+ single_limit_sat: number;
613
+ status: 'active' | 'frozen' | 'banned';
614
+ /**
615
+ * TOFU-bound originator signer address (EVM, lowercase 0x…) for the
616
+ * WeChat fiat auth flow. Null for legacy accounts never seen with a
617
+ * signature; bound on first signed request. @see ./auth.ts
618
+ */
619
+ signer_address: string | null;
620
+ /**
621
+ * Gateway-attested WeChat payer openid that funded this account, recorded
622
+ * on top-up confirm. Anchors the custodial balance to a real WeChat user.
623
+ * Null for accounts never funded via WeChat (crypto/alipay/operator).
624
+ */
625
+ wechat_openid: string | null;
626
+ created_at: string;
627
+ updated_at: string;
628
+ }
629
+ /** Ledger transaction row (amounts in integer cents, always positive). */
630
+ interface LedgerTxRow {
631
+ id: string;
632
+ buyer_id: string;
633
+ type: 'topup' | 'deduct' | 'refund';
634
+ amount_sat: number;
635
+ service: string | null;
636
+ description: string | null;
637
+ /** Client-supplied idempotency key (deducts). Unique when present. */
638
+ request_id: string | null;
639
+ /** External settlement reference: on-chain tx hash / Alipay or WeChat trade no. */
640
+ external_ref: string | null;
641
+ /** For refunds: the deduct row being reversed. */
642
+ refunds_tx_id: string | null;
643
+ status: 'completed' | 'refunded';
644
+ created_at: string;
645
+ }
646
+ type DeductErrorCode = 'buyer_not_found' | 'buyer_not_active' | 'insufficient_balance' | 'exceeds_single_limit' | 'exceeds_daily_limit';
647
+ interface DeductResult {
648
+ success: boolean;
649
+ /** Ledger transaction id (also returned on an idempotent replay). */
650
+ txId?: string;
651
+ /** True when `request_id` matched an existing deduct — nothing was charged. */
652
+ replayed?: boolean;
653
+ balanceSat?: number;
654
+ error?: DeductErrorCode;
655
+ /** Populated for limit errors. */
656
+ limitSat?: number;
657
+ }
658
+ interface DeductOpts {
659
+ buyerId: string;
660
+ amountSat: number;
661
+ /** Idempotency key; a replay returns the original tx without deducting. */
662
+ requestId?: string;
663
+ service?: string;
664
+ description?: string;
665
+ }
666
+ interface TopupOpts {
667
+ buyerId: string;
668
+ amountSat: number;
669
+ /** On-chain tx hash / Alipay trade_no / WeChat out_trade_no. Unique — a
670
+ * replayed reference credits nothing and returns the original row. */
671
+ externalRef: string;
672
+ description?: string;
673
+ }
674
+ interface RefundResult {
675
+ success: boolean;
676
+ txId?: string;
677
+ balanceSat?: number;
678
+ /** True when the deduct was already refunded — nothing was credited. */
679
+ replayed?: boolean;
680
+ error?: 'tx_not_found' | 'not_a_deduct';
681
+ }
682
+ interface LedgerConfig {
683
+ /** SQLite file path, or ':memory:' for tests. */
684
+ dbPath: string;
685
+ /** Default per-transaction limit for new buyers, integer cents. */
686
+ defaultSingleLimitSat?: number;
687
+ /** Default daily limit for new buyers, integer cents. */
688
+ defaultDailyLimitSat?: number;
689
+ /**
690
+ * Ledger quote currency (e.g. 'USD', 'CNY'). Recorded once in `ledger_meta`
691
+ * on first init and enforced on every subsequent open: the minor unit
692
+ * (`*_sat`) is 1/100 of this currency, so reopening a USD ledger as CNY would
693
+ * silently reinterpret every balance. Defaults to "USD".
694
+ */
695
+ currency?: string;
696
+ }
697
+ /** Convert a decimal string/number of dollars to integer cents. Throws on >2dp. */
698
+ declare function toSat(amount: string | number): number;
699
+ /** Convert integer cents back to a 2-decimal string. */
700
+ declare function fromSat(sat: number): string;
701
+ declare class BalanceLedger {
702
+ private readonly db;
703
+ private readonly defaultSingleLimitSat;
704
+ private readonly defaultDailyLimitSat;
705
+ constructor(config: LedgerConfig);
706
+ /** Fetch a buyer, or null. */
707
+ getBuyer(buyerId: string): BuyerRow | null;
708
+ /** Fetch a buyer, creating an empty active account on first sight. */
709
+ getOrCreateBuyer(buyerId: string, displayName?: string): BuyerRow;
710
+ /**
711
+ * Record the gateway-attested WeChat payer openid that funded a buyer, on
712
+ * top-up confirm. Idempotent and observational (stage 1a): it never
713
+ * overwrites an existing binding with a *different* openid — a conflict is
714
+ * reported to the caller (possible account sharing / spoof) but not
715
+ * enforced here. Creates the buyer if absent.
716
+ */
717
+ bindOpenid(buyerId: string, openid: string): {
718
+ bound: boolean;
719
+ conflict: boolean;
720
+ existing: string | null;
721
+ };
722
+ /**
723
+ * TOFU-bind the account's spending signer address (EVM, lowercase). First
724
+ * signed request records it; later requests must match. A mismatch is
725
+ * reported (`conflict`) so the caller can reject under `enforce` — it is
726
+ * never silently overwritten. Creates the buyer if absent.
727
+ */
728
+ bindSigner(buyerId: string, address: string): {
729
+ bound: boolean;
730
+ conflict: boolean;
731
+ existing: string | null;
732
+ };
733
+ /** Sum of today's (UTC) completed deducts minus refunds issued against them. */
734
+ spentTodaySat(buyerId: string): number;
735
+ /**
736
+ * Read-only deduction precheck (the rail's `verify`). Never mutates.
737
+ * Returns the same error codes `deduct` would.
738
+ */
739
+ checkDeduct(buyerId: string, amountSat: number): DeductResult;
740
+ /**
741
+ * Atomic deduction (the rail's `settle`). Check + UPDATE run inside one
742
+ * SQLite transaction; a `request_id` replay returns the original tx
743
+ * without charging again.
744
+ */
745
+ deduct(opts: DeductOpts): DeductResult;
746
+ /**
747
+ * Credit a top-up. `externalRef` is the settlement proof reference
748
+ * (on-chain tx hash / fiat trade number) and is unique: replaying the same
749
+ * reference returns the original row without crediting twice.
750
+ */
751
+ topup(opts: TopupOpts): {
752
+ txId: string;
753
+ balanceSat: number;
754
+ replayed?: boolean;
755
+ };
756
+ /**
757
+ * Reverse a deduct (service failed after the charge). Idempotent: the
758
+ * unique index on `refunds_tx_id` means a second refund of the same deduct
759
+ * returns the original refund row.
760
+ */
761
+ refund(deductTxId: string, reason?: string): RefundResult;
762
+ /** Paged transaction history, newest first (rowid breaks same-second ties). */
763
+ listTransactions(buyerId: string, limit?: number, offset?: number): LedgerTxRow[];
764
+ /** Quick integrity probe for healthCheck(). */
765
+ integrityOk(): boolean;
766
+ close(): void;
767
+ }
768
+
769
+ /** Domain tag — separates these signatures from any other message a key signs. */
770
+ declare const BALANCE_AUTH_DOMAIN = "moltspay-balance-auth:v1";
771
+ /** Accepted clock skew between client timestamp and server (replay window). */
772
+ declare const BALANCE_AUTH_MAX_SKEW_MS: number;
773
+ /** Rollout gate for balance-rail user auth. */
774
+ type BalanceAuthMode = 'off' | 'shadow' | 'enforce';
775
+ /** Signature material carried in the balance X-Payment payload. */
776
+ interface BalanceAuthFields {
777
+ /** Unix seconds when the client signed. */
778
+ timestamp: number;
779
+ /** EIP-191 signature over {@link buildDeductMessage}. */
780
+ signature: string;
781
+ }
782
+ /**
783
+ * Canonical message a client signs to authorize one deduction. Both sides
784
+ * MUST build this identically. `amount` is intentionally omitted — the
785
+ * service id determines the price server-side, and `request_id` (unique per
786
+ * charge) + `timestamp` (windowed) already bound replay.
787
+ */
788
+ declare function buildDeductMessage(f: {
789
+ buyerId: string;
790
+ requestId: string;
791
+ service: string;
792
+ timestamp: number;
793
+ }): string;
794
+ interface AuthVerifyResult {
795
+ ok: boolean;
796
+ /** Recovered signer address (lowercase 0x…), present when the signature parsed. */
797
+ recovered?: string;
798
+ /** Failure code when `ok` is false: no_signature | malformed | timestamp_skew | bad_signature. */
799
+ reason?: string;
800
+ }
801
+ /**
802
+ * Verify a deduction signature and recover the signer address. Never throws.
803
+ * Does NOT decide binding/enforcement — the caller compares `recovered`
804
+ * against the account's bound signer and applies `auth_mode`.
805
+ */
806
+ declare function verifyDeductAuth(opts: {
807
+ auth: BalanceAuthFields | null;
808
+ buyerId: string;
809
+ requestId: string;
810
+ service: string;
811
+ nowMs: number;
812
+ }): AuthVerifyResult;
813
+
814
+ /**
815
+ * Custodial Balance Facilitator (password-free rail).
816
+ *
817
+ * Third payment mode beside per-transaction crypto signing and fiat QR
818
+ * (Alipay/WeChat): buyers top up once into a server-custodied SQLite ledger
819
+ * and subsequent purchases are deducted directly — no signature, no scan.
820
+ *
821
+ * Interface mapping (differs from the QR rails — see BALANCE-RAIL-DESIGN.md):
822
+ * - `createPaymentRequirements` is **pure**: it formats the `accepts[]` entry
823
+ * without any I/O. Nothing is minted per 402, so the WeChat rail's
824
+ * order-per-challenge double-charge class of bug cannot occur here.
825
+ * - `verify` is a read-only funds/limits precheck.
826
+ * - `settle` is **the atomic deduction** (single SQLite transaction),
827
+ * idempotent on the client-supplied `request_id`.
828
+ * - `refund` (rail-specific, not on the Facilitator interface) reverses a
829
+ * deduct when the skill fails after the charge.
830
+ *
831
+ * Execution order at the server is therefore inverted relative to QR rails:
832
+ * QR: verify(paid?) → run skill → settle (confirm, fire-and-forget)
833
+ * balance: verify(funds?) → settle (deduct) → run skill → [fail → refund]
834
+ *
835
+ * @see ./balance/ledger.ts — SQLite ledger (atomicity, idempotency, limits)
836
+ * @see ../../docs/BALANCE-RAIL-DESIGN.md — design
837
+ */
838
+
839
+ /** Network identifier exposed via `Facilitator.supportedNetworks`. */
840
+ declare const BALANCE_NETWORK = "balance";
841
+ /** x402 `scheme` string identifying the custodial balance rail in `accepts[]`. */
842
+ declare const BALANCE_SCHEME = "balance";
843
+
844
+ /**
845
+ * Facilitator-level config sourced from `provider.balance` in
846
+ * `moltspay.services.json`.
847
+ */
848
+ interface BalanceFacilitatorConfig {
849
+ /** SQLite ledger file path (created on first init). ':memory:' for tests. */
850
+ db_path: string;
851
+ /** Ledger quote currency. Default 'USD'. */
852
+ currency?: string;
853
+ /** Default per-transaction limit for new buyers, decimal string. Default "5.00". */
854
+ single_limit?: string;
855
+ /** Default daily limit for new buyers, decimal string. Default "10.00". */
856
+ daily_limit?: string;
857
+ /**
858
+ * User-auth rollout gate for deductions. Default 'off' (bare buyer_id, as
859
+ * before). 'shadow' verifies + TOFU-binds the signer but never blocks;
860
+ * 'enforce' rejects unsigned/mismatched deductions.
861
+ */
862
+ auth_mode?: BalanceAuthMode;
863
+ }
864
+ /** The buyer-identifying payload carried in X-Payment for this rail. */
865
+ interface BalancePaymentPayload {
866
+ buyer_id: string;
867
+ /** Client-generated idempotency key: replays never double-deduct. */
868
+ request_id?: string;
869
+ /** Originator signature (present when the client signs). @see ./balance/auth.ts */
870
+ auth?: BalanceAuthFields;
871
+ }
872
+ /** Extract {buyer_id, request_id, auth} from an x402 payload, or null. */
873
+ declare function extractBalancePayload(payment: X402PaymentPayload): BalancePaymentPayload | null;
874
+ /**
875
+ * Custodial balance facilitator. Construction opens (or creates) the SQLite
876
+ * ledger; on Node < 22.5 it throws with an actionable message.
877
+ */
878
+ declare class BalanceFacilitator extends BaseFacilitator {
879
+ readonly name = "balance";
880
+ readonly displayName = "Custodial Balance";
881
+ readonly supportedNetworks: string[];
882
+ readonly currency: string;
883
+ /** User-auth rollout gate for deductions (off | shadow | enforce). */
884
+ readonly authMode: BalanceAuthMode;
885
+ private readonly ledger;
886
+ constructor(config: BalanceFacilitatorConfig);
887
+ /** Direct ledger access for the server's balance-management endpoints. */
888
+ getLedger(): BalanceLedger;
889
+ /**
890
+ * Build the `accepts[]` entry for a service. Pure — no I/O, nothing minted.
891
+ */
892
+ createPaymentRequirements(opts: {
893
+ price: string;
894
+ serviceId?: string;
895
+ }): X402PaymentRequirements;
896
+ /** Read-only funds/limits precheck. Never mutates the ledger. */
897
+ verify(paymentPayload: X402PaymentPayload, requirements: X402PaymentRequirements): Promise<VerifyResult>;
898
+ /**
899
+ * The atomic deduction. Idempotent on `request_id`; the returned
900
+ * `transaction` is the ledger tx id (usable for `refund`).
901
+ */
902
+ settle(paymentPayload: X402PaymentPayload, requirements: X402PaymentRequirements): Promise<SettleResult>;
903
+ /** Reverse a deduct after a downstream failure. Idempotent per deduct. */
904
+ refund(deductTxId: string, reason?: string): RefundResult;
905
+ /**
906
+ * Credit a gateway-verified external settlement to a buyer's balance. The
907
+ * single entry point for every funding path -- WeChat callback, WeChat
908
+ * polling, and the operator `/balance/topup` endpoint -- so they share one
909
+ * idempotency boundary: `externalRef` is unique in the ledger, so a replay
910
+ * credits nothing and returns the original transaction (`replayed: true`).
911
+ * Callers must pass a gateway-verified `amountSat`, never a client-declared
912
+ * amount.
913
+ */
914
+ credit(opts: {
915
+ buyerId: string;
916
+ amountSat: number;
917
+ externalRef: string;
918
+ description?: string;
919
+ }): {
920
+ txId: string;
921
+ balance: string;
922
+ balanceSat: number;
923
+ replayed: boolean;
924
+ };
925
+ healthCheck(): Promise<HealthCheckResult>;
926
+ private describeDeductError;
927
+ }
928
+
929
+ export { ALIPAY_GATEWAY_PROD, ALIPAY_GATEWAY_SANDBOX, ALIPAY_NETWORK, ALIPAY_SCHEME, AlipayFacilitator, type AlipayFacilitatorConfig, type AlipayPaymentProof, type AlipayPaymentRequirements, BALANCE_AUTH_DOMAIN, BALANCE_AUTH_MAX_SKEW_MS, BALANCE_NETWORK, BALANCE_SCHEME, BNBFacilitator, type BNBPaymentIntent, type BalanceAuthFields, type BalanceAuthMode, BalanceFacilitator, type BalanceFacilitatorConfig, BalanceLedger, type BalancePaymentPayload, BaseFacilitator, type BuyerRow, type CreatePaymentRequirementsOpts$1 as CreatePaymentRequirementsOpts, type DeductResult, HealthCheckResult, type LedgerTxRow, type RefundResult, SettleResult, SolanaFacilitator, type SolanaPaymentPayload, TempoFacilitator, VerifyResult, WECHAT_AMOUNT_REGEX, WECHAT_API_BASE, WECHAT_NETWORK, WECHAT_SCHEME, WECHAT_TIME_EXPIRE_MS, WechatFacilitator, type WechatFacilitatorConfig, type WechatPaymentRequirements, X402PaymentPayload, X402PaymentRequirements, buildDeductMessage, createIntentTypedData, createSolanaPaymentTransaction, extractBalancePayload, fromSat, parseWechatAttach, toSat, verifyDeductAuth };