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.
- package/.env.example +23 -0
- package/CHANGELOG.md +551 -0
- package/README.md +466 -7
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +102 -1
- package/dist/chains/index.d.ts +102 -1
- package/dist/chains/index.js +66 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +6850 -2127
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6857 -2128
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +348 -1
- package/dist/client/index.d.ts +348 -1
- package/dist/client/index.js +1462 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1469 -51
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +17 -1
- package/dist/client/web/index.mjs +11 -0
- package/dist/client/web/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +663 -4
- package/dist/facilitators/index.d.ts +663 -4
- package/dist/facilitators/index.js +1292 -10
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +1268 -9
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +4229 -556
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4214 -548
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1462 -45
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1473 -56
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +336 -2
- package/dist/server/index.d.ts +336 -2
- package/dist/server/index.js +2516 -188
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +2516 -188
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +20 -2
- package/schemas/moltspay.services.schema.json +211 -6
- package/scripts/postinstall.js +91 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as BaseFacilitator, H as HealthCheckResult, X as X402PaymentPayload,
|
|
2
|
-
export {
|
|
3
|
-
export { C as CDPFacilitator, a as CDPFacilitatorConfig } from '../cdp-
|
|
1
|
+
import { B as BaseFacilitator, H as HealthCheckResult, X as X402PaymentPayload, a as X402PaymentRequirements, V as VerifyResult, S as SettleResult } from '../registry-DIo0WNoO.js';
|
|
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.js';
|
|
3
|
+
export { C as CDPFacilitator, a as CDPFacilitatorConfig } from '../cdp-B5PhDUTC.js';
|
|
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
|
-
|
|
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 };
|