@vobs/payment 0.3.0 → 1.2.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/LICENSE +1 -1
- package/README.md +182 -0
- package/dist/alipay/client.cjs +112 -0
- package/dist/alipay/client.cjs.map +1 -0
- package/dist/alipay/client.d.cts +36 -0
- package/dist/alipay/client.d.ts +36 -0
- package/dist/alipay/client.js +110 -0
- package/dist/alipay/client.js.map +1 -0
- package/dist/alipay/config.cjs +67 -0
- package/dist/alipay/config.cjs.map +1 -0
- package/dist/alipay/config.d.cts +30 -0
- package/dist/alipay/config.d.ts +30 -0
- package/dist/alipay/config.js +65 -0
- package/dist/alipay/config.js.map +1 -0
- package/dist/alipay/index.cjs +592 -0
- package/dist/alipay/index.cjs.map +1 -0
- package/dist/alipay/index.d.cts +12 -0
- package/dist/alipay/index.d.ts +12 -0
- package/dist/alipay/index.js +580 -0
- package/dist/alipay/index.js.map +1 -0
- package/dist/alipay/notify.cjs +70 -0
- package/dist/alipay/notify.cjs.map +1 -0
- package/dist/alipay/notify.d.cts +52 -0
- package/dist/alipay/notify.d.ts +52 -0
- package/dist/alipay/notify.js +68 -0
- package/dist/alipay/notify.js.map +1 -0
- package/dist/alipay/payment.cjs +49 -0
- package/dist/alipay/payment.cjs.map +1 -0
- package/dist/alipay/payment.d.cts +24 -0
- package/dist/alipay/payment.d.ts +24 -0
- package/dist/alipay/payment.js +47 -0
- package/dist/alipay/payment.js.map +1 -0
- package/dist/alipay/plugin.cjs +300 -0
- package/dist/alipay/plugin.cjs.map +1 -0
- package/dist/alipay/plugin.d.cts +43 -0
- package/dist/alipay/plugin.d.ts +43 -0
- package/dist/alipay/plugin.js +296 -0
- package/dist/alipay/plugin.js.map +1 -0
- package/dist/alipay/query.cjs +80 -0
- package/dist/alipay/query.cjs.map +1 -0
- package/dist/alipay/query.d.cts +26 -0
- package/dist/alipay/query.d.ts +26 -0
- package/dist/alipay/query.js +78 -0
- package/dist/alipay/query.js.map +1 -0
- package/dist/alipay/refund.cjs +104 -0
- package/dist/alipay/refund.cjs.map +1 -0
- package/dist/alipay/refund.d.cts +32 -0
- package/dist/alipay/refund.d.ts +32 -0
- package/dist/alipay/refund.js +102 -0
- package/dist/alipay/refund.js.map +1 -0
- package/dist/alipay/response.cjs +38 -0
- package/dist/alipay/response.cjs.map +1 -0
- package/dist/alipay/response.d.cts +19 -0
- package/dist/alipay/response.d.ts +19 -0
- package/dist/alipay/response.js +35 -0
- package/dist/alipay/response.js.map +1 -0
- package/dist/alipay/types.cjs +4 -0
- package/dist/alipay/types.cjs.map +1 -0
- package/dist/alipay/types.d.cts +242 -0
- package/dist/alipay/types.d.ts +242 -0
- package/dist/alipay/types.js +3 -0
- package/dist/alipay/types.js.map +1 -0
- package/dist/alipay/wap.cjs +50 -0
- package/dist/alipay/wap.cjs.map +1 -0
- package/dist/alipay/wap.d.cts +24 -0
- package/dist/alipay/wap.d.ts +24 -0
- package/dist/alipay/wap.js +48 -0
- package/dist/alipay/wap.js.map +1 -0
- package/dist/index-B1aUR7Vt.d.ts +49 -0
- package/dist/index-B8pzZxGq.d.cts +44 -0
- package/dist/index-BWnIrwRi.d.ts +44 -0
- package/dist/index-Dyrkl86r.d.cts +49 -0
- package/dist/index.cjs +1071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -37
- package/dist/index.js +1052 -37
- package/dist/index.js.map +1 -0
- package/dist/wechat/client.cjs +119 -0
- package/dist/wechat/client.cjs.map +1 -0
- package/dist/wechat/client.d.cts +39 -0
- package/dist/wechat/client.d.ts +39 -0
- package/dist/wechat/client.js +117 -0
- package/dist/wechat/client.js.map +1 -0
- package/dist/wechat/config.cjs +76 -0
- package/dist/wechat/config.cjs.map +1 -0
- package/dist/wechat/config.d.cts +29 -0
- package/dist/wechat/config.d.ts +29 -0
- package/dist/wechat/config.js +74 -0
- package/dist/wechat/config.js.map +1 -0
- package/dist/wechat/index.cjs +612 -0
- package/dist/wechat/index.cjs.map +1 -0
- package/dist/wechat/index.d.cts +10 -0
- package/dist/wechat/index.d.ts +10 -0
- package/dist/wechat/index.js +602 -0
- package/dist/wechat/index.js.map +1 -0
- package/dist/wechat/notify.cjs +73 -0
- package/dist/wechat/notify.cjs.map +1 -0
- package/dist/wechat/notify.d.cts +53 -0
- package/dist/wechat/notify.d.ts +53 -0
- package/dist/wechat/notify.js +71 -0
- package/dist/wechat/notify.js.map +1 -0
- package/dist/wechat/payment.cjs +186 -0
- package/dist/wechat/payment.cjs.map +1 -0
- package/dist/wechat/payment.d.cts +50 -0
- package/dist/wechat/payment.d.ts +50 -0
- package/dist/wechat/payment.js +184 -0
- package/dist/wechat/payment.js.map +1 -0
- package/dist/wechat/plugin.cjs +305 -0
- package/dist/wechat/plugin.cjs.map +1 -0
- package/dist/wechat/plugin.d.cts +40 -0
- package/dist/wechat/plugin.d.ts +40 -0
- package/dist/wechat/plugin.js +301 -0
- package/dist/wechat/plugin.js.map +1 -0
- package/dist/wechat/refund.cjs +112 -0
- package/dist/wechat/refund.cjs.map +1 -0
- package/dist/wechat/refund.d.cts +26 -0
- package/dist/wechat/refund.d.ts +26 -0
- package/dist/wechat/refund.js +110 -0
- package/dist/wechat/refund.js.map +1 -0
- package/dist/wechat/response.cjs +50 -0
- package/dist/wechat/response.cjs.map +1 -0
- package/dist/wechat/response.d.cts +20 -0
- package/dist/wechat/response.d.ts +20 -0
- package/dist/wechat/response.js +46 -0
- package/dist/wechat/response.js.map +1 -0
- package/dist/wechat/types.cjs +4 -0
- package/dist/wechat/types.cjs.map +1 -0
- package/dist/wechat/types.d.cts +200 -0
- package/dist/wechat/types.d.ts +200 -0
- package/dist/wechat/types.js +3 -0
- package/dist/wechat/types.js.map +1 -0
- package/package.json +37 -28
- package/src/alipay/alipay.test.ts +231 -0
- package/src/alipay/client.ts +67 -0
- package/src/alipay/config.ts +72 -0
- package/src/alipay/index.ts +32 -0
- package/src/alipay/notify.ts +92 -0
- package/src/alipay/payment.ts +60 -0
- package/src/alipay/plugin.ts +64 -0
- package/src/alipay/query.ts +52 -0
- package/src/alipay/refund.ts +85 -0
- package/src/alipay/response.ts +40 -0
- package/src/alipay/types.ts +271 -0
- package/src/alipay/wap.ts +55 -0
- package/src/index.ts +78 -0
- package/src/wechat/client.ts +68 -0
- package/src/wechat/config.ts +81 -0
- package/src/wechat/index.ts +30 -0
- package/src/wechat/notify.ts +93 -0
- package/src/wechat/payment.ts +177 -0
- package/src/wechat/plugin.ts +59 -0
- package/src/wechat/refund.ts +80 -0
- package/src/wechat/response.ts +54 -0
- package/src/wechat/types.ts +223 -0
- package/src/wechat/wechat.test.ts +393 -0
- package/dist/alipay-official.d.ts +0 -34
- package/dist/alipay-official.js +0 -206
- package/dist/handlers.d.ts +0 -45
- package/dist/handlers.js +0 -105
- package/dist/jeepay.d.ts +0 -39
- package/dist/jeepay.js +0 -206
- package/dist/lakala.d.ts +0 -41
- package/dist/lakala.js +0 -243
- package/dist/manager.d.ts +0 -67
- package/dist/manager.js +0 -538
- package/dist/mock-driver.d.ts +0 -22
- package/dist/mock-driver.js +0 -59
- package/dist/state-machine.d.ts +0 -41
- package/dist/state-machine.js +0 -60
- package/dist/store.d.ts +0 -33
- package/dist/store.js +0 -76
- package/dist/types.d.ts +0 -168
- package/dist/types.js +0 -11
- package/dist/verify.d.ts +0 -34
- package/dist/verify.js +0 -55
- package/dist/wechat-official.d.ts +0 -40
- package/dist/wechat-official.js +0 -208
package/dist/state-machine.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Order state machine (pure functions, no I/O).
|
|
7
|
-
*
|
|
8
|
-
* Transitions (no refund path):
|
|
9
|
-
* created ──normal callback──▶ paid (idempotent: a paid order ignores repeated callbacks)
|
|
10
|
-
* created ──close───▶ closed ──callback again──▶ frozen (payment after close = money-losing anomaly, manual review)
|
|
11
|
-
* created ──amount mismatch──▶ frozen (conflict freeze: never auto-book)
|
|
12
|
-
* created ──paid ≥ auditThreshold──▶ pending (large amount, manual review)──confirm──▶ paid
|
|
13
|
-
* zero-amount orders are paid immediately (no channel)
|
|
14
|
-
*/
|
|
15
|
-
import type { OrderState } from './types.js';
|
|
16
|
-
export type CallbackTransition = {
|
|
17
|
-
readonly state: OrderState;
|
|
18
|
-
readonly reason?: string;
|
|
19
|
-
} | {
|
|
20
|
-
readonly state: 'paid';
|
|
21
|
-
readonly idempotent: boolean;
|
|
22
|
-
readonly reason?: string;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Apply a payment callback to an order.
|
|
26
|
-
* @param orderState current state
|
|
27
|
-
* @param orderAmount order amount (fen)
|
|
28
|
-
* @param paidAmount channel-paid amount (fen)
|
|
29
|
-
* @param auditThreshold large-amount audit threshold (fen); absent/0 = no audit
|
|
30
|
-
*/
|
|
31
|
-
export declare function applyPaidCallback(orderState: OrderState, orderAmount: number, paidAmount: number, auditThreshold?: number): CallbackTransition;
|
|
32
|
-
/** Approve a large amount: only pending (audit) → paid. */
|
|
33
|
-
export declare function applyConfirm(orderState: OrderState): {
|
|
34
|
-
state: OrderState;
|
|
35
|
-
ok: boolean;
|
|
36
|
-
};
|
|
37
|
-
/** Close on timeout/cancel: only created → closed. */
|
|
38
|
-
export declare function applyClose(orderState: OrderState): {
|
|
39
|
-
state: OrderState;
|
|
40
|
-
ok: boolean;
|
|
41
|
-
};
|
package/dist/state-machine.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Order state machine (pure functions, no I/O).
|
|
7
|
-
*
|
|
8
|
-
* Transitions (no refund path):
|
|
9
|
-
* created ──normal callback──▶ paid (idempotent: a paid order ignores repeated callbacks)
|
|
10
|
-
* created ──close───▶ closed ──callback again──▶ frozen (payment after close = money-losing anomaly, manual review)
|
|
11
|
-
* created ──amount mismatch──▶ frozen (conflict freeze: never auto-book)
|
|
12
|
-
* created ──paid ≥ auditThreshold──▶ pending (large amount, manual review)──confirm──▶ paid
|
|
13
|
-
* zero-amount orders are paid immediately (no channel)
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Apply a payment callback to an order.
|
|
17
|
-
* @param orderState current state
|
|
18
|
-
* @param orderAmount order amount (fen)
|
|
19
|
-
* @param paidAmount channel-paid amount (fen)
|
|
20
|
-
* @param auditThreshold large-amount audit threshold (fen); absent/0 = no audit
|
|
21
|
-
*/
|
|
22
|
-
export function applyPaidCallback(orderState, orderAmount, paidAmount, auditThreshold) {
|
|
23
|
-
// freeze on conflict: paid ≠ order amount (security red line, never auto-book; discount net amounts are judged by the channel, differences go through amountTolerance evaluated by the business)
|
|
24
|
-
if (paidAmount !== orderAmount) {
|
|
25
|
-
return { state: 'frozen', reason: 'amount_mismatch' };
|
|
26
|
-
}
|
|
27
|
-
switch (orderState) {
|
|
28
|
-
case 'paid':
|
|
29
|
-
// idempotent: don't book twice (callback retry/redelivery)
|
|
30
|
-
return { state: 'paid', idempotent: true };
|
|
31
|
-
case 'closed':
|
|
32
|
-
// payment after close → anomaly, manual review
|
|
33
|
-
return { state: 'frozen', reason: 'paid_after_close' };
|
|
34
|
-
case 'frozen':
|
|
35
|
-
return { state: 'frozen' };
|
|
36
|
-
case 'pending':
|
|
37
|
-
return { state: 'pending' };
|
|
38
|
-
default: {
|
|
39
|
-
// created: large amount awaits manual audit (pending = 'unconfirmed': channel delay or audit)
|
|
40
|
-
if (auditThreshold && auditThreshold > 0 && paidAmount >= auditThreshold) {
|
|
41
|
-
return { state: 'pending', reason: 'audit' };
|
|
42
|
-
}
|
|
43
|
-
return { state: 'paid' };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** Approve a large amount: only pending (audit) → paid. */
|
|
48
|
-
export function applyConfirm(orderState) {
|
|
49
|
-
if (orderState === 'pending') {
|
|
50
|
-
return { state: 'paid', ok: true };
|
|
51
|
-
}
|
|
52
|
-
return { state: orderState, ok: false };
|
|
53
|
-
}
|
|
54
|
-
/** Close on timeout/cancel: only created → closed. */
|
|
55
|
-
export function applyClose(orderState) {
|
|
56
|
-
if (orderState === 'created') {
|
|
57
|
-
return { state: 'closed', ok: true };
|
|
58
|
-
}
|
|
59
|
-
return { state: orderState, ok: false };
|
|
60
|
-
}
|
package/dist/store.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Order storage abstraction (P0 in-memory; for a production DB see the design doc's production checklist:
|
|
7
|
-
* unique constraints back idempotency, row locks/CAS prevent concurrent double-writes, idempotency records and nonces with TTL).
|
|
8
|
-
*/
|
|
9
|
-
import type { AuditEntry, OrderRecord } from './types.js';
|
|
10
|
-
export interface PaymentStore {
|
|
11
|
-
getOrder(orderId: string): OrderRecord | undefined;
|
|
12
|
-
/** Atomically reserves an order id before a channel call. Returns false when it is already reserved or persisted. */
|
|
13
|
-
reserveOrder(orderId: string): boolean;
|
|
14
|
-
/** Releases an unpersisted reservation after a failed create attempt. */
|
|
15
|
-
releaseOrderReservation(orderId: string): void;
|
|
16
|
-
saveOrder(order: OrderRecord): void;
|
|
17
|
-
/** Iterate all orders (for expiry scans; production implementations should paginate/index). */
|
|
18
|
-
allOrders(): readonly OrderRecord[];
|
|
19
|
-
/** nonce dedup (anti-replay): returns true on first use. */
|
|
20
|
-
addNonce(nonce: string): boolean;
|
|
21
|
-
getOrderByIdempotencyKey(key: string): string | undefined;
|
|
22
|
-
setIdempotencyKey(key: string, orderId: string): void;
|
|
23
|
-
appendAudit(entry: AuditEntry): void;
|
|
24
|
-
auditLog(): readonly AuditEntry[];
|
|
25
|
-
}
|
|
26
|
-
export interface MemoryStoreOptions {
|
|
27
|
-
/** Retention for callback nonces and idempotency keys. Defaults to 24 hours. */
|
|
28
|
-
readonly retentionMs?: number;
|
|
29
|
-
/** Maximum retained audit entries. Defaults to 10,000. */
|
|
30
|
-
readonly maxAuditEntries?: number;
|
|
31
|
-
}
|
|
32
|
-
/** In-memory implementation. Production stores must back reserveOrder with a unique constraint or transaction. */
|
|
33
|
-
export declare function createMemoryStore(options?: MemoryStoreOptions): PaymentStore;
|
package/dist/store.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Order storage abstraction (P0 in-memory; for a production DB see the design doc's production checklist:
|
|
7
|
-
* unique constraints back idempotency, row locks/CAS prevent concurrent double-writes, idempotency records and nonces with TTL).
|
|
8
|
-
*/
|
|
9
|
-
/** In-memory implementation. Production stores must back reserveOrder with a unique constraint or transaction. */
|
|
10
|
-
export function createMemoryStore(options = {}) {
|
|
11
|
-
const retentionMs = options.retentionMs ?? 24 * 60 * 60 * 1000;
|
|
12
|
-
const maxAuditEntries = options.maxAuditEntries ?? 10_000;
|
|
13
|
-
const orders = new Map();
|
|
14
|
-
const reservations = new Set();
|
|
15
|
-
const nonces = new Map();
|
|
16
|
-
const idempotency = new Map();
|
|
17
|
-
const audit = [];
|
|
18
|
-
const pruneExpired = (now) => {
|
|
19
|
-
for (const [nonce, expiresAt] of nonces) {
|
|
20
|
-
if (expiresAt <= now)
|
|
21
|
-
nonces.delete(nonce);
|
|
22
|
-
}
|
|
23
|
-
for (const [key, entry] of idempotency) {
|
|
24
|
-
if (entry.expiresAt <= now)
|
|
25
|
-
idempotency.delete(key);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
return {
|
|
29
|
-
getOrder(orderId) {
|
|
30
|
-
return orders.get(orderId);
|
|
31
|
-
},
|
|
32
|
-
reserveOrder(orderId) {
|
|
33
|
-
if (orders.has(orderId) || reservations.has(orderId))
|
|
34
|
-
return false;
|
|
35
|
-
reservations.add(orderId);
|
|
36
|
-
return true;
|
|
37
|
-
},
|
|
38
|
-
releaseOrderReservation(orderId) {
|
|
39
|
-
reservations.delete(orderId);
|
|
40
|
-
},
|
|
41
|
-
saveOrder(order) {
|
|
42
|
-
orders.set(order.orderId, order);
|
|
43
|
-
reservations.delete(order.orderId);
|
|
44
|
-
},
|
|
45
|
-
allOrders() {
|
|
46
|
-
return Array.from(orders.values());
|
|
47
|
-
},
|
|
48
|
-
addNonce(nonce) {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
pruneExpired(now);
|
|
51
|
-
if (nonces.has(nonce))
|
|
52
|
-
return false;
|
|
53
|
-
nonces.set(nonce, now + retentionMs);
|
|
54
|
-
return true;
|
|
55
|
-
},
|
|
56
|
-
getOrderByIdempotencyKey(key) {
|
|
57
|
-
const now = Date.now();
|
|
58
|
-
pruneExpired(now);
|
|
59
|
-
return idempotency.get(key)?.orderId;
|
|
60
|
-
},
|
|
61
|
-
setIdempotencyKey(key, orderId) {
|
|
62
|
-
const now = Date.now();
|
|
63
|
-
pruneExpired(now);
|
|
64
|
-
idempotency.set(key, { orderId, expiresAt: now + retentionMs });
|
|
65
|
-
},
|
|
66
|
-
appendAudit(entry) {
|
|
67
|
-
audit.push(entry);
|
|
68
|
-
if (audit.length > maxAuditEntries) {
|
|
69
|
-
audit.splice(0, audit.length - maxAuditEntries);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
auditLog() {
|
|
73
|
-
return audit;
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @vobs/payment contract types (aligned with docs/development/payment-factory-design.md §3/§5).
|
|
7
|
-
*
|
|
8
|
-
* Scope boundary: the payment factory only does **forward collection** (order → verify → book → reconcile).
|
|
9
|
-
* Refunds are out of scope — business refunds go directly to the channel's refund API.
|
|
10
|
-
*/
|
|
11
|
-
/** Payment scenes (channel support matrix). */
|
|
12
|
-
export type PaymentScene = 'pc' | 'h5' | 'miniapp' | 'app';
|
|
13
|
-
/** Order states (no refund path). */
|
|
14
|
-
export type OrderState = 'created' | 'pending' | 'paid' | 'closed' | 'frozen';
|
|
15
|
-
/**
|
|
16
|
-
* Channel order input.
|
|
17
|
-
* `orderId` is globally unique — used as the channel's `out_trade_no` (channel-side unique constraint = natural idempotency, safe to retry after disconnects).
|
|
18
|
-
*/
|
|
19
|
-
export interface CreateOrderInput {
|
|
20
|
-
readonly orderId: string;
|
|
21
|
-
/** Fen (amounts are always integers to avoid float errors; currency CNY, extensible for multi-currency). */
|
|
22
|
-
readonly amount: number;
|
|
23
|
-
readonly subject: string;
|
|
24
|
-
readonly scene: PaymentScene;
|
|
25
|
-
readonly notifyUrl: string;
|
|
26
|
-
readonly extras?: Readonly<Record<string, unknown>>;
|
|
27
|
-
}
|
|
28
|
-
/** One-time payment params (the frontend initiates payment with these; keys never reach the frontend). */
|
|
29
|
-
export type OrderInit = {
|
|
30
|
-
readonly kind: 'jsapi';
|
|
31
|
-
readonly payParams: Readonly<Record<string, string>>;
|
|
32
|
-
} | {
|
|
33
|
-
readonly kind: 'qr';
|
|
34
|
-
readonly qrUrl: string;
|
|
35
|
-
readonly expiresAt?: string;
|
|
36
|
-
} | {
|
|
37
|
-
readonly kind: 'redirect';
|
|
38
|
-
readonly payUrl: string;
|
|
39
|
-
};
|
|
40
|
-
/** Callback authentication owner. Unspecified drivers use the manager HMAC protocol. */
|
|
41
|
-
export type PaymentCallbackVerification = 'manager-hmac' | 'driver';
|
|
42
|
-
/** Unified payment driver contract (three capabilities: createOrder / verifyCallback / queryOrder). */
|
|
43
|
-
export interface PaymentDriver {
|
|
44
|
-
readonly name: string;
|
|
45
|
-
readonly scenes: readonly PaymentScene[];
|
|
46
|
-
/**
|
|
47
|
-
* Declares who authenticates callbacks. Real channel drivers use `driver`; custom drivers
|
|
48
|
-
* default to the manager HMAC protocol and therefore require the X-* callback headers.
|
|
49
|
-
*/
|
|
50
|
-
readonly callbackVerification?: PaymentCallbackVerification;
|
|
51
|
-
/** Place an order: call the payment channel → return one-time payment params. */
|
|
52
|
-
createOrder(input: CreateOrderInput): Promise<OrderInit>;
|
|
53
|
-
/** Channel callback verification + normalization: NOTIFY request → verify channel signature → normalize order result. */
|
|
54
|
-
verifyCallback(payload: unknown, headers: unknown): VerifiedCallback;
|
|
55
|
-
/** Query order status (reconciliation/compensation). */
|
|
56
|
-
queryOrder(orderId: string): Promise<OrderStatus>;
|
|
57
|
-
/**
|
|
58
|
-
* Channel-agreed callback success text (e.g. WeChat/Jeepay `SUCCESS`, Alipay `success`).
|
|
59
|
-
* Default = JSON response (mock/self-built backends). After a successful callback the HTTP layer
|
|
60
|
-
* returns this channel-agreed format so the channel doesn't misjudge failure and retry.
|
|
61
|
-
*/
|
|
62
|
-
readonly successResponseText?: string;
|
|
63
|
-
}
|
|
64
|
-
/** Normalized channel callback result (amounts always in fen). */
|
|
65
|
-
export interface VerifiedCallback {
|
|
66
|
-
readonly ok: boolean;
|
|
67
|
-
readonly reason?: string;
|
|
68
|
-
readonly orderId?: string;
|
|
69
|
-
readonly paidAmount?: number;
|
|
70
|
-
readonly channelTxnId?: string;
|
|
71
|
-
}
|
|
72
|
-
export type OrderStatus = {
|
|
73
|
-
readonly state: OrderState;
|
|
74
|
-
readonly paidAt?: string;
|
|
75
|
-
} | {
|
|
76
|
-
readonly state: 'unknown';
|
|
77
|
-
readonly raw: unknown;
|
|
78
|
-
};
|
|
79
|
-
/** Order record (payment-factory internal state, stored in PaymentStore). */
|
|
80
|
-
export interface OrderRecord {
|
|
81
|
-
readonly orderId: string;
|
|
82
|
-
readonly amount: number;
|
|
83
|
-
readonly subject: string;
|
|
84
|
-
readonly scene: PaymentScene;
|
|
85
|
-
readonly driver: string;
|
|
86
|
-
state: OrderState;
|
|
87
|
-
paidAt?: string;
|
|
88
|
-
createdAt: number;
|
|
89
|
-
init?: OrderInit;
|
|
90
|
-
channelTxnId?: string;
|
|
91
|
-
/** Large-amount audit pending reason (audit). */
|
|
92
|
-
auditReason?: string;
|
|
93
|
-
/** Business delivery state after a paid transition. Failed delivery remains pending for retry. */
|
|
94
|
-
deliveryState?: 'pending' | 'delivered';
|
|
95
|
-
deliveryError?: string;
|
|
96
|
-
}
|
|
97
|
-
/** Global policy config (PaymentManager-level, driver-agnostic). */
|
|
98
|
-
export interface PaymentConfig {
|
|
99
|
-
/** Strategy when an unpaid order already exists for the same business order. Default 'ask'. */
|
|
100
|
-
readonly duplicateOrderStrategy?: 'reuse' | 'replace' | 'reject' | 'ask';
|
|
101
|
-
/** Large-amount audit threshold (fen): callback succeeds but paid ≥ this → stay pending until manually confirmed. */
|
|
102
|
-
readonly auditThreshold?: number;
|
|
103
|
-
/** Per-order limit (fen): reject over-limit orders (channel limits are usually lower; configure per driver). */
|
|
104
|
-
readonly maxAmount?: number;
|
|
105
|
-
/** Callback time window (sec) for anti-replay. Default 300. */
|
|
106
|
-
readonly timeWindowSec?: number;
|
|
107
|
-
/** Retention (ms) for idempotency records / callback nonces. Default 24h. */
|
|
108
|
-
readonly idempotencyTtlMs?: number;
|
|
109
|
-
}
|
|
110
|
-
/** Create-order HTTP input. `amount` is only a display/consistency check — the server-side price wins (server-pricing red line). */
|
|
111
|
-
export interface CreateOrderRequest {
|
|
112
|
-
readonly orderId: string;
|
|
113
|
-
readonly amount: number;
|
|
114
|
-
readonly subject: string;
|
|
115
|
-
readonly scene?: PaymentScene;
|
|
116
|
-
readonly driver?: string;
|
|
117
|
-
readonly duplicateStrategy?: 'reuse' | 'replace' | 'reject' | 'ask';
|
|
118
|
-
readonly idempotencyKey?: string;
|
|
119
|
-
}
|
|
120
|
-
/** Create-order result. */
|
|
121
|
-
export type CreateOrderResult = {
|
|
122
|
-
readonly kind: 'ok';
|
|
123
|
-
readonly driver: string;
|
|
124
|
-
readonly orderId: string;
|
|
125
|
-
readonly state: OrderState;
|
|
126
|
-
readonly init?: OrderInit;
|
|
127
|
-
} | {
|
|
128
|
-
readonly kind: 'duplicate';
|
|
129
|
-
readonly existingOrderId: string;
|
|
130
|
-
readonly existingAmount: number;
|
|
131
|
-
};
|
|
132
|
-
/** Callback handling result (incl. HTTP response protocol: persist before responding). */
|
|
133
|
-
export interface CallbackResult {
|
|
134
|
-
readonly ok: boolean;
|
|
135
|
-
readonly status: number;
|
|
136
|
-
readonly body: Readonly<Record<string, unknown>>;
|
|
137
|
-
readonly orderId?: string;
|
|
138
|
-
readonly state?: OrderState;
|
|
139
|
-
readonly reason?: string;
|
|
140
|
-
/** Channel-agreed success text (WeChat/Jeepay `SUCCESS`, Alipay `success`) — when present the HTTP layer returns plain text instead of JSON. */
|
|
141
|
-
readonly text?: string;
|
|
142
|
-
}
|
|
143
|
-
/** Notification after booking (onVerified hook — business deliver/activate; must be idempotent). */
|
|
144
|
-
export interface VerifiedResult {
|
|
145
|
-
readonly orderId: string;
|
|
146
|
-
readonly state: OrderState;
|
|
147
|
-
readonly paidAt?: string;
|
|
148
|
-
readonly channelTxnId?: string;
|
|
149
|
-
}
|
|
150
|
-
/** Alert (onAlert hook — triggered on frozen/conflict/large-amount/signature failure). */
|
|
151
|
-
export interface PaymentAlert {
|
|
152
|
-
readonly level: 'warn' | 'error';
|
|
153
|
-
readonly code: string;
|
|
154
|
-
readonly message: string;
|
|
155
|
-
readonly orderId?: string;
|
|
156
|
-
readonly driver?: string;
|
|
157
|
-
}
|
|
158
|
-
/** Operation audit entry (create/callback-book/freeze/confirm/delivery all logged). */
|
|
159
|
-
export interface AuditEntry {
|
|
160
|
-
readonly operation: 'create' | 'callback' | 'close' | 'confirm' | 'freeze' | 'delivery';
|
|
161
|
-
readonly orderId?: string;
|
|
162
|
-
readonly from?: OrderState;
|
|
163
|
-
readonly to?: OrderState;
|
|
164
|
-
readonly reason?: string;
|
|
165
|
-
readonly at: number;
|
|
166
|
-
}
|
|
167
|
-
/** Server-pricing hook (business: orderId → amount; undefined = order not found). */
|
|
168
|
-
export type GetOrderAmount = (orderId: string) => number | undefined | Promise<number | undefined>;
|
package/dist/types.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @vobs/payment contract types (aligned with docs/development/payment-factory-design.md §3/§5).
|
|
7
|
-
*
|
|
8
|
-
* Scope boundary: the payment factory only does **forward collection** (order → verify → book → reconcile).
|
|
9
|
-
* Refunds are out of scope — business refunds go directly to the channel's refund API.
|
|
10
|
-
*/
|
|
11
|
-
export {};
|
package/dist/verify.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Callback verification + anti-replay utilities.
|
|
7
|
-
*
|
|
8
|
-
* Protocol: `X-Signature = hex( HMAC-SHA256( apiKey, "{timestamp}.{nonce}.{raw body}" ) )`
|
|
9
|
-
* Check order:
|
|
10
|
-
* ① |now - timestamp| ≤ timeWindowSec else timestamp_expired (time-window anti-replay)
|
|
11
|
-
* ② signature matches else bad_signature
|
|
12
|
-
* ③ nonce unused (deduped by the caller via store) else nonce_reused (anti-replay)
|
|
13
|
-
*
|
|
14
|
-
* Idempotency prevents double-booking; anti-replay prevents forged replays — both are required.
|
|
15
|
-
*/
|
|
16
|
-
import type { PaymentConfig } from './types.js';
|
|
17
|
-
export type VerifyResult = {
|
|
18
|
-
readonly ok: true;
|
|
19
|
-
} | {
|
|
20
|
-
readonly ok: false;
|
|
21
|
-
readonly reason: string;
|
|
22
|
-
};
|
|
23
|
-
/** HMAC-SHA256 hex (Web Crypto, globally available in Node 22+). */
|
|
24
|
-
export declare function hmacSha256Hex(key: string, data: string): Promise<string>;
|
|
25
|
-
/** Callback verification + time-window check (nonce dedup is the caller's job — needs concurrency-safe storage). */
|
|
26
|
-
export declare function verifySignedCallback(opts: {
|
|
27
|
-
readonly apiKey: string;
|
|
28
|
-
readonly timestamp: string;
|
|
29
|
-
readonly nonce: string;
|
|
30
|
-
readonly signature: string;
|
|
31
|
-
readonly body: string;
|
|
32
|
-
readonly timeWindowSec?: number;
|
|
33
|
-
}): Promise<VerifyResult>;
|
|
34
|
-
export declare function timeWindowOf(config: PaymentConfig | undefined): number | undefined;
|
package/dist/verify.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Callback verification + anti-replay utilities.
|
|
7
|
-
*
|
|
8
|
-
* Protocol: `X-Signature = hex( HMAC-SHA256( apiKey, "{timestamp}.{nonce}.{raw body}" ) )`
|
|
9
|
-
* Check order:
|
|
10
|
-
* ① |now - timestamp| ≤ timeWindowSec else timestamp_expired (time-window anti-replay)
|
|
11
|
-
* ② signature matches else bad_signature
|
|
12
|
-
* ③ nonce unused (deduped by the caller via store) else nonce_reused (anti-replay)
|
|
13
|
-
*
|
|
14
|
-
* Idempotency prevents double-booking; anti-replay prevents forged replays — both are required.
|
|
15
|
-
*/
|
|
16
|
-
const DEFAULT_TIME_WINDOW_SEC = 300;
|
|
17
|
-
/** HMAC-SHA256 hex (Web Crypto, globally available in Node 22+). */
|
|
18
|
-
export async function hmacSha256Hex(key, data) {
|
|
19
|
-
const keyBytes = new TextEncoder().encode(key);
|
|
20
|
-
const dataBytes = new TextEncoder().encode(data);
|
|
21
|
-
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
22
|
-
const sig = await crypto.subtle.sign('HMAC', cryptoKey, dataBytes);
|
|
23
|
-
return Array.from(new Uint8Array(sig))
|
|
24
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
25
|
-
.join('');
|
|
26
|
-
}
|
|
27
|
-
/** Callback verification + time-window check (nonce dedup is the caller's job — needs concurrency-safe storage). */
|
|
28
|
-
export async function verifySignedCallback(opts) {
|
|
29
|
-
const windowSec = opts.timeWindowSec ?? DEFAULT_TIME_WINDOW_SEC;
|
|
30
|
-
const ts = Number(opts.timestamp);
|
|
31
|
-
if (!Number.isFinite(ts)) {
|
|
32
|
-
return { ok: false, reason: 'bad_timestamp' };
|
|
33
|
-
}
|
|
34
|
-
if (Math.abs(Date.now() / 1000 - ts) > windowSec) {
|
|
35
|
-
return { ok: false, reason: 'timestamp_expired' };
|
|
36
|
-
}
|
|
37
|
-
const expected = await hmacSha256Hex(opts.apiKey, `${opts.timestamp}.${opts.nonce}.${opts.body}`);
|
|
38
|
-
if (!timingSafeEqual(expected, opts.signature)) {
|
|
39
|
-
return { ok: false, reason: 'bad_signature' };
|
|
40
|
-
}
|
|
41
|
-
return { ok: true };
|
|
42
|
-
}
|
|
43
|
-
/** Constant-time comparison to prevent timing side channels. */
|
|
44
|
-
function timingSafeEqual(a, b) {
|
|
45
|
-
if (a.length !== b.length)
|
|
46
|
-
return false;
|
|
47
|
-
let diff = 0;
|
|
48
|
-
for (let i = 0; i < a.length; i++) {
|
|
49
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
50
|
-
}
|
|
51
|
-
return diff === 0;
|
|
52
|
-
}
|
|
53
|
-
export function timeWindowOf(config) {
|
|
54
|
-
return config?.timeWindowSec;
|
|
55
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* WeChat Pay official driver (API v3) — Native (PC scan) / JSAPI (official account, miniapp webview) / H5.
|
|
7
|
-
*
|
|
8
|
-
* Covered scenes (web):
|
|
9
|
-
* - `pc` → Native payment → `{kind:'qr', qrUrl: code_url}`
|
|
10
|
-
* - `h5` + openid → JSAPI payment → `{kind:'jsapi', payParams}` (official account / WeChat built-in browser)
|
|
11
|
-
* - `h5` (no openid) → H5 payment → `{kind:'redirect', payUrl: h5_url}`
|
|
12
|
-
* - `miniapp` + openid → JSAPI payment (miniapp webview)
|
|
13
|
-
*
|
|
14
|
-
* Security:
|
|
15
|
-
* - Request signing: Authorization `WECHATPAY2-SHA256-RSA2048` (merchant private key SHA256withRSA)
|
|
16
|
-
* - Callback: AES-256-GCM decryption (apiV3Key) + optional platform-cert verification (enforced when platformPublicKeyPem is set) + time-window/nonce anti-replay
|
|
17
|
-
* - Channel self-verification path: callbacks bypass the framework's generic HMAC protocol; this driver verifies the channel itself
|
|
18
|
-
*
|
|
19
|
-
* Dependencies: node:crypto (Node built-in, zero third-party). Sandbox needs sandbox keys/certs from the merchant platform (configure baseUrl per the sandbox docs when integrating).
|
|
20
|
-
*/
|
|
21
|
-
import type { PaymentDriver } from './types.js';
|
|
22
|
-
import type { PaymentStore } from './store.js';
|
|
23
|
-
export interface WechatOfficialOptions {
|
|
24
|
-
readonly appId: string;
|
|
25
|
-
readonly mchId: string;
|
|
26
|
-
/** APIv3 key (32-byte string, used for AES-GCM callback decryption). Inject via KMS/env in production. */
|
|
27
|
-
readonly apiV3Key: string;
|
|
28
|
-
/** Merchant API certificate private key (PEM). */
|
|
29
|
-
readonly privateKeyPem: string;
|
|
30
|
-
/** Merchant API certificate serial number. */
|
|
31
|
-
readonly serialNo: string;
|
|
32
|
-
/** Platform public key (PEM, optional) — when set, callbacks require platform-cert verification; otherwise rely on AES-GCM decryption (apiV3Key confidentiality) + anti-replay. */
|
|
33
|
-
readonly platformPublicKeyPem?: string;
|
|
34
|
-
/** Callback time window (sec, anti-replay). Default 300. */
|
|
35
|
-
readonly timeWindowSec?: number;
|
|
36
|
-
/** Channel gateway URL (sandbox/prod). Defaults to prod. */
|
|
37
|
-
readonly baseUrl?: string;
|
|
38
|
-
readonly fetchImpl?: typeof fetch;
|
|
39
|
-
}
|
|
40
|
-
export declare function createWechatOfficialDriver(store: PaymentStore, options: WechatOfficialOptions): PaymentDriver;
|