@seasonkoh/webaz 0.1.28 → 0.1.30

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 (198) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/bond-refund-blockers.js +50 -0
  4. package/dist/bond-slash.js +100 -0
  5. package/dist/bond-terms.js +31 -0
  6. package/dist/currency.js +16 -0
  7. package/dist/deposit-rails.js +52 -0
  8. package/dist/direct-pay-aml-monitor.js +67 -0
  9. package/dist/direct-pay-aml-review.js +40 -0
  10. package/dist/direct-pay-base-bond-entry.js +5 -0
  11. package/dist/direct-pay-bond-rail-clearance.js +99 -0
  12. package/dist/direct-pay-cancel-refund.js +160 -0
  13. package/dist/direct-pay-compliance-ingress.js +145 -0
  14. package/dist/direct-pay-controls.js +136 -0
  15. package/dist/direct-pay-create.js +231 -0
  16. package/dist/direct-pay-deferral-quota.js +43 -0
  17. package/dist/direct-pay-disclosures.js +50 -0
  18. package/dist/direct-pay-eligibility.js +46 -0
  19. package/dist/direct-pay-fee-ar.js +241 -0
  20. package/dist/direct-pay-fee-prepay-request.js +80 -0
  21. package/dist/direct-pay-launch-readiness.js +108 -0
  22. package/dist/direct-pay-launch-summary.js +68 -0
  23. package/dist/direct-pay-ledger.js +94 -0
  24. package/dist/direct-pay-returns.js +104 -0
  25. package/dist/direct-pay-stock.js +9 -0
  26. package/dist/direct-receive-account-qr.js +77 -0
  27. package/dist/direct-receive-accounts.js +82 -0
  28. package/dist/direct-receive-deferral.js +171 -0
  29. package/dist/direct-receive-deposits.js +365 -0
  30. package/dist/direct-receive-payment-instruction.js +26 -0
  31. package/dist/free-shipping.js +37 -0
  32. package/dist/fx-rates.js +78 -0
  33. package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
  34. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  35. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  36. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
  37. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  38. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  39. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  40. package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
  41. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  42. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  43. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  44. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  45. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  46. package/dist/ledger.js +12 -3
  47. package/dist/mcp.js +23 -4
  48. package/dist/merchant-bond-domain.js +64 -0
  49. package/dist/merchant-bond-exposure.js +78 -0
  50. package/dist/merchant-bond-watcher.js +26 -0
  51. package/dist/payment-rails.js +29 -0
  52. package/dist/platform-receive-accounts.js +94 -0
  53. package/dist/product-verification.js +93 -0
  54. package/dist/pwa/acp-feed.js +3 -2
  55. package/dist/pwa/arbitration-read-admin.js +37 -0
  56. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  57. package/dist/pwa/contract-fingerprint.js +18 -0
  58. package/dist/pwa/direct-pay-guards.js +20 -0
  59. package/dist/pwa/direct-pay-order-redaction.js +42 -0
  60. package/dist/pwa/endpoint-actions.js +3 -0
  61. package/dist/pwa/human-presence.js +2 -6
  62. package/dist/pwa/public/app-account.js +9 -9
  63. package/dist/pwa/public/app-admin-disputes.js +55 -0
  64. package/dist/pwa/public/app-agent-appeal.js +90 -0
  65. package/dist/pwa/public/app-agent-approvals.js +93 -0
  66. package/dist/pwa/public/app-agent-pair.js +127 -0
  67. package/dist/pwa/public/app-ai.js +10 -10
  68. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  69. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  70. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  71. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  72. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  73. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  74. package/dist/pwa/public/app-bond-ui.js +108 -0
  75. package/dist/pwa/public/app-chat-poll.js +29 -0
  76. package/dist/pwa/public/app-contribution-hub.js +23 -0
  77. package/dist/pwa/public/app-create-kinds.js +17 -0
  78. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  79. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  80. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  81. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  82. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  83. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  84. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  85. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  86. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  87. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  88. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  89. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  90. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  91. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  92. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  93. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  94. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  95. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  96. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  97. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  98. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  99. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  100. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  101. package/dist/pwa/public/app-direct-pay.js +226 -0
  102. package/dist/pwa/public/app-discover.js +20 -20
  103. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  104. package/dist/pwa/public/app-external-links.js +32 -0
  105. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  106. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  107. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  108. package/dist/pwa/public/app-listings.js +4 -4
  109. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  110. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  111. package/dist/pwa/public/app-notif-templates.js +22 -0
  112. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  113. package/dist/pwa/public/app-order-errors.js +50 -0
  114. package/dist/pwa/public/app-order-labels.js +19 -0
  115. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  116. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  117. package/dist/pwa/public/app-poll-governor.js +22 -0
  118. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  119. package/dist/pwa/public/app-price.js +55 -0
  120. package/dist/pwa/public/app-product-media.js +15 -0
  121. package/dist/pwa/public/app-profile.js +10 -10
  122. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  123. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  124. package/dist/pwa/public/app-seller.js +5 -5
  125. package/dist/pwa/public/app-shop.js +19 -19
  126. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  127. package/dist/pwa/public/app.js +264 -295
  128. package/dist/pwa/public/i18n.js +1068 -197
  129. package/dist/pwa/public/index.html +58 -0
  130. package/dist/pwa/public/openapi.json +1845 -653
  131. package/dist/pwa/public/style.css +3 -0
  132. package/dist/pwa/routes/admin-analytics.js +4 -2
  133. package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
  134. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  135. package/dist/pwa/routes/admin-reports.js +31 -5
  136. package/dist/pwa/routes/agent-governance.js +1 -1
  137. package/dist/pwa/routes/agent-grants.js +253 -32
  138. package/dist/pwa/routes/analytics.js +2 -0
  139. package/dist/pwa/routes/arbitrator.js +67 -14
  140. package/dist/pwa/routes/bond-seller.js +162 -0
  141. package/dist/pwa/routes/buyer-feeds.js +2 -1
  142. package/dist/pwa/routes/chat.js +6 -1
  143. package/dist/pwa/routes/dashboards.js +2 -1
  144. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  145. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  146. package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
  147. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  148. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  149. package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
  150. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  151. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  152. package/dist/pwa/routes/disputes-read.js +20 -6
  153. package/dist/pwa/routes/disputes-write.js +91 -33
  154. package/dist/pwa/routes/external-anchors.js +4 -2
  155. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  156. package/dist/pwa/routes/fx.js +12 -0
  157. package/dist/pwa/routes/governance-onboarding.js +46 -8
  158. package/dist/pwa/routes/leaderboard.js +47 -9
  159. package/dist/pwa/routes/listings.js +2 -2
  160. package/dist/pwa/routes/logistics.js +6 -2
  161. package/dist/pwa/routes/manifests.js +38 -0
  162. package/dist/pwa/routes/me-data.js +5 -2
  163. package/dist/pwa/routes/mutual-cancel.js +62 -0
  164. package/dist/pwa/routes/orders-action.js +297 -16
  165. package/dist/pwa/routes/orders-create.js +21 -6
  166. package/dist/pwa/routes/orders-read.js +106 -8
  167. package/dist/pwa/routes/p2p-products.js +2 -2
  168. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  169. package/dist/pwa/routes/products-create.js +15 -4
  170. package/dist/pwa/routes/products-links.js +34 -0
  171. package/dist/pwa/routes/products-list.js +2 -1
  172. package/dist/pwa/routes/products-update.js +37 -3
  173. package/dist/pwa/routes/profile-identity.js +4 -2
  174. package/dist/pwa/routes/promoter.js +3 -0
  175. package/dist/pwa/routes/referral.js +4 -0
  176. package/dist/pwa/routes/returns.js +60 -4
  177. package/dist/pwa/routes/rewards-apply.js +10 -5
  178. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  179. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  180. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  181. package/dist/pwa/routes/seller-quota.js +5 -1
  182. package/dist/pwa/routes/shipping-templates.js +219 -0
  183. package/dist/pwa/routes/shops.js +2 -1
  184. package/dist/pwa/routes/snf.js +4 -1
  185. package/dist/pwa/routes/url-claim.js +2 -2
  186. package/dist/pwa/routes/users-public.js +8 -0
  187. package/dist/pwa/routes/wallet-read.js +3 -0
  188. package/dist/pwa/routes/webauthn.js +3 -3
  189. package/dist/pwa/server.js +112 -132
  190. package/dist/runtime/agent-grant-scopes.js +69 -1
  191. package/dist/runtime/webaz-schema-helpers.js +161 -3
  192. package/dist/sale-regions.js +116 -0
  193. package/dist/shipping-templates.js +119 -0
  194. package/dist/store-verification.js +77 -0
  195. package/dist/trade-tax.js +99 -0
  196. package/dist/trade-terms.js +76 -0
  197. package/dist/version.js +1 -1
  198. package/package.json +128 -2
@@ -0,0 +1,156 @@
1
+ import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
2
+ import { toUnits, toDecimal } from '../../money.js';
3
+ import { applyWalletDelta, walletUnits } from '../../ledger.js';
4
+ export function initMutualCancelSchema(db) {
5
+ db.exec(`CREATE TABLE IF NOT EXISTS mutual_cancel_proposals (
6
+ id TEXT PRIMARY KEY,
7
+ order_id TEXT NOT NULL,
8
+ dispute_id TEXT NOT NULL,
9
+ proposed_by TEXT NOT NULL,
10
+ counterparty TEXT NOT NULL,
11
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | declined | withdrawn
12
+ reason TEXT,
13
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14
+ resolved_at TEXT,
15
+ resolved_by TEXT
16
+ )`);
17
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_mutual_cancel_order ON mutual_cancel_proposals(order_id, status)`);
18
+ }
19
+ /** 载入订单+活跃争议,并判定 caller 是否当事方 + 订单是否处于可协商取消的状态。fail-closed。 */
20
+ function loadCancellable(db, orderId, userId) {
21
+ const order = db.prepare('SELECT id, buyer_id, seller_id, status, payment_rail, total_amount, stake_backing, bid_stake_held FROM orders WHERE id = ?').get(orderId);
22
+ if (!order)
23
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
24
+ const role = userId === order.buyer_id ? 'buyer' : userId === order.seller_id ? 'seller' : null;
25
+ if (!role)
26
+ return { ok: false, error: '仅买卖双方可协商取消该订单', error_code: 'NOT_A_PARTY' };
27
+ if (order.status !== 'disputed')
28
+ return { ok: false, error: '仅争议中的订单可协商取消', error_code: 'ORDER_NOT_DISPUTED' };
29
+ const dispute = db.prepare("SELECT id, status FROM disputes WHERE order_id = ? AND status IN ('open','in_review') ORDER BY created_at DESC LIMIT 1").get(orderId);
30
+ if (!dispute)
31
+ return { ok: false, error: '该订单没有进行中的争议', error_code: 'NO_ACTIVE_DISPUTE' };
32
+ return { ok: true, order, dispute, role };
33
+ }
34
+ /** 当前 pending 提议(至多一条)。 */
35
+ function pendingProposal(db, orderId) {
36
+ return db.prepare("SELECT id, proposed_by, counterparty, status, reason, created_at FROM mutual_cancel_proposals WHERE order_id = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 1").get(orderId);
37
+ }
38
+ /** 提议协商取消。proposalId 由路由用 generateId 生成后传入(域模块不自造 id)。 */
39
+ export function proposeMutualCancel(db, orderId, userId, reason, proposalId) {
40
+ const g = loadCancellable(db, orderId, userId);
41
+ if (!g.ok)
42
+ return g;
43
+ const existing = pendingProposal(db, orderId);
44
+ if (existing) {
45
+ if (existing.proposed_by === userId)
46
+ return { ok: false, error: '你已提议协商取消,正在等待对方确认', error_code: 'ALREADY_PROPOSED', proposal_id: existing.id };
47
+ return { ok: false, error: '对方已提议协商取消,请直接确认接受', error_code: 'COUNTERPARTY_ALREADY_PROPOSED', proposal_id: existing.id };
48
+ }
49
+ const counterparty = g.role === 'buyer' ? g.order.seller_id : g.order.buyer_id;
50
+ db.prepare("INSERT INTO mutual_cancel_proposals (id, order_id, dispute_id, proposed_by, counterparty, status, reason) VALUES (?,?,?,?,?,'pending',?)")
51
+ .run(proposalId, orderId, g.dispute.id, userId, counterparty, (reason || '').slice(0, 500) || null);
52
+ return { ok: true, proposal_id: proposalId, status: 'pending' };
53
+ }
54
+ /** 提议方撤回自己的 pending 提议。 */
55
+ export function withdrawMutualCancel(db, orderId, userId) {
56
+ const prop = pendingProposal(db, orderId);
57
+ if (!prop)
58
+ return { ok: false, error: '没有待处理的协商取消提议', error_code: 'NO_PENDING_PROPOSAL' };
59
+ if (prop.proposed_by !== userId)
60
+ return { ok: false, error: '只能撤回自己提出的提议', error_code: 'NOT_PROPOSER' };
61
+ db.prepare("UPDATE mutual_cancel_proposals SET status='withdrawn', resolved_at=datetime('now'), resolved_by=? WHERE id=?").run(userId, prop.id);
62
+ return { ok: true, status: 'withdrawn' };
63
+ }
64
+ /** 对方拒绝 pending 提议。 */
65
+ export function declineMutualCancel(db, orderId, userId) {
66
+ const g = loadCancellable(db, orderId, userId);
67
+ if (!g.ok)
68
+ return g;
69
+ const prop = pendingProposal(db, orderId);
70
+ if (!prop)
71
+ return { ok: false, error: '没有待处理的协商取消提议', error_code: 'NO_PENDING_PROPOSAL' };
72
+ if (prop.proposed_by === userId)
73
+ return { ok: false, error: '不能拒绝自己的提议(如需取消请撤回)', error_code: 'CANNOT_DECLINE_OWN' };
74
+ db.prepare("UPDATE mutual_cancel_proposals SET status='declined', resolved_at=datetime('now'), resolved_by=? WHERE id=?").run(userId, prop.id);
75
+ return { ok: true, status: 'declined' };
76
+ }
77
+ /**
78
+ * 资金+状态结算(tx-free core)——【必须由路由用 db.transaction 包裹】。
79
+ * · 直付(非托管):零资金,仅 transition→cancelled。
80
+ * · 托管:买家托管货款退回(escrowed→balance);卖家【本订单实际锁定】的质押返还(staked→balance)。无罚没/手续费/信誉。
81
+ * · 【money-path 铁律】买家退款 FAIL-CLOSED:实际 escrowed 必须 ≥ 本单全额,否则中止(不退/不关单/不结案),
82
+ * 绝不部分退款还静默收口。卖家质押返还取订单快照 stake_backing + bid_stake_held(不是商品名义 stake_amount ——
83
+ * 当前 escrow 模型 stake_backing 恒 0、不锁卖家 stake,bid_stake_held 仅中标单有),cap 到卖家真实 staked
84
+ * (best-effort,不 fail-closed,以免卖家侧账目瑕疵卡死买家退款)。两侧都绝不打负 / 凭空印钱。
85
+ * · 争议行同事务置 resolved(verdict='mutual_cancel'),防自动裁决二次结算。
86
+ */
87
+ function settleMutualCancel(db, order, disputeId) {
88
+ const sys = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
89
+ if (!sys)
90
+ return { ok: false, error: 'sys_protocol 用户不存在', error_code: 'SYS_MISSING' };
91
+ const nonCustodial = order.payment_rail === 'direct_p2p';
92
+ let detail;
93
+ if (nonCustodial) {
94
+ detail = { non_custodial: true, buyer_refund: 0, seller_stake_returned: 0, note: '非托管(直付)单:协议不持货款 → 零资金,仅关单' };
95
+ }
96
+ else {
97
+ // 买家 made-whole 不可妥协 —— FAIL-CLOSED:买家实际 escrowed 必须 ≥ 本单全额,否则【中止】(不退、不关单、
98
+ // 不结案,留人工核对账目),绝不"部分退款还静默收口"。足额时退【本单全额】(escrowed≥total 保证退后 escrowed≥0,不超退)。
99
+ const totalU = toUnits(order.total_amount);
100
+ if (walletUnits(db, order.buyer_id).escrowed < totalU)
101
+ return { ok: false, error: '买家托管余额不足以全额退款,协商取消已中止(账目待人工核对)', error_code: 'ESCROW_INSUFFICIENT' };
102
+ applyWalletDelta(db, order.buyer_id, { escrowed: -totalU, balance: totalU });
103
+ // 卖家质押返还 = 尽力而为的抵押物返还(与买家退款不同、【不】fail-closed,以免卖家侧账目瑕疵卡死买家的无责退款):
104
+ // 只退【本订单快照锁定】的质押(stake_backing+bid_stake_held,非商品名义值),再 cap 到卖家真实 staked → 绝不打负/印钱。
105
+ const lockedU = toUnits(Number(order.stake_backing || 0)) + toUnits(Number(order.bid_stake_held || 0));
106
+ const stakeReturnU = Math.max(0, Math.min(lockedU, walletUnits(db, order.seller_id).staked));
107
+ if (stakeReturnU > 0)
108
+ applyWalletDelta(db, order.seller_id, { staked: -stakeReturnU, balance: stakeReturnU });
109
+ detail = { non_custodial: false, buyer_refund: toDecimal(totalU), seller_stake_returned: toDecimal(stakeReturnU), note: '托管单:买家全额退款(fail-closed 已验足额)+ 卖家本单锁定质押返还(cap 实际 staked);无罚没/手续费/信誉' };
110
+ }
111
+ const tr = transition(db, order.id, 'cancelled', sys.id, [], '双方协商取消(无责裁定:买家退款,双方信誉不受影响)');
112
+ if (!tr.success)
113
+ return { ok: false, error: tr.error, error_code: 'TRANSITION_FAILED' };
114
+ db.prepare("UPDATE disputes SET status='resolved', verdict='mutual_cancel', verdict_reason=?, ruling_type='mutual_cancel', resolved_at=datetime('now') WHERE id=?")
115
+ .run('双方合意取消订单 —— 无责终结,不计入任一方信誉', disputeId);
116
+ return { ok: true, settlement: { ...detail, terminal: 'cancelled', dispute: 'resolved', fault: 'none', reputation_impact: 'none' } };
117
+ }
118
+ /**
119
+ * 对方确认接受 → 执行协商取消。【路由必须 db.transaction 包裹本函数】。
120
+ * 事务内重新校验(competition-safe):当事方 + order 仍 disputed + 争议仍 open|in_review + 存在 pending 提议 + 非自我确认。
121
+ */
122
+ export function acceptMutualCancel(db, orderId, userId) {
123
+ const g = loadCancellable(db, orderId, userId);
124
+ if (!g.ok)
125
+ return g;
126
+ const prop = pendingProposal(db, orderId);
127
+ if (!prop)
128
+ return { ok: false, error: '没有待确认的协商取消提议', error_code: 'NO_PENDING_PROPOSAL' };
129
+ if (prop.proposed_by === userId)
130
+ return { ok: false, error: '不能确认自己提出的取消,需由对方确认', error_code: 'CANNOT_ACCEPT_OWN' };
131
+ const settle = settleMutualCancel(db, g.order, g.dispute.id);
132
+ if (!settle.ok)
133
+ return settle;
134
+ db.prepare("UPDATE mutual_cancel_proposals SET status='accepted', resolved_at=datetime('now'), resolved_by=? WHERE id=?").run(userId, prop.id);
135
+ return { ok: true, status: 'accepted', settlement: settle.settlement };
136
+ }
137
+ /** UI 状态:当前提议 + 该 caller 可执行的动作。【party-gated】:非当事人一律 NOT_A_PARTY,绝不泄露提议/理由/发起方/存在性。 */
138
+ export function getMutualCancelState(db, orderId, userId) {
139
+ const order = db.prepare('SELECT buyer_id, seller_id, status FROM orders WHERE id = ?').get(orderId);
140
+ if (!order)
141
+ return { ok: false, error: '订单不存在', error_code: 'ORDER_NOT_FOUND' };
142
+ if (userId !== order.buyer_id && userId !== order.seller_id)
143
+ return { ok: false, error: '仅买卖双方可查看协商取消状态', error_code: 'NOT_A_PARTY' }; // 读也 party-gate:不向非当事人泄露提议/理由/发起方
144
+ const hasActiveDispute = !!db.prepare("SELECT 1 FROM disputes WHERE order_id = ? AND status IN ('open','in_review') LIMIT 1").get(orderId);
145
+ const eligible = order.status === 'disputed' && hasActiveDispute;
146
+ const prop = pendingProposal(db, orderId);
147
+ const proposal = prop ? { id: prop.id, proposed_by: prop.proposed_by, reason: prop.reason, created_at: prop.created_at, mine: prop.proposed_by === userId } : null;
148
+ return {
149
+ ok: true,
150
+ proposal,
151
+ can_propose: eligible && !prop,
152
+ can_accept: eligible && !!prop && prop.proposed_by !== userId,
153
+ can_decline: eligible && !!prop && prop.proposed_by !== userId,
154
+ can_withdraw: eligible && !!prop && prop.proposed_by === userId,
155
+ };
156
+ }
@@ -5,7 +5,7 @@
5
5
  * 核心思路:解决冷启动——现有 Amazon/Shopify 卖家零成本接入新渠道。
6
6
  *
7
7
  * Skill 类型:
8
- * catalog_sync - 目录同步(把外部店铺接入 DCP 搜索)
8
+ * catalog_sync - 目录同步(把外部店铺接入 WebAZ 搜索)
9
9
  * auto_accept - 自动接单(买家下单后立即接受,无需等待)
10
10
  * price_negotiation - 价格协商(允许 Agent 在限定范围内议价)
11
11
  * quality_guarantee - 质量承诺(额外质押,增强买家信心)
@@ -67,8 +67,8 @@ export const SKILL_TYPE_META = {
67
67
  label: '目录同步',
68
68
  label_en: 'Catalog sync',
69
69
  icon: '🔄',
70
- description: '将卖家的外部商品目录(Amazon/Shopify/自定义)同步到 DCP,买家 Agent 订阅后可优先发现这些商品。',
71
- description_en: 'Sync seller external catalogs (Amazon/Shopify/custom) to DCP — subscribed buyer agents discover these first.',
70
+ description: '将卖家的外部商品目录(Amazon/Shopify/自定义)同步到 WebAZ,买家 Agent 订阅后可优先发现这些商品。',
71
+ description_en: 'Sync seller external catalogs (Amazon/Shopify/custom) to WebAZ — subscribed buyer agents discover these first.',
72
72
  },
73
73
  auto_accept: {
74
74
  label: '自动接单',
@@ -95,7 +95,7 @@ export const SKILL_TYPE_META = {
95
95
  label: '质量承诺',
96
96
  label_en: 'Quality guarantee',
97
97
  icon: '🛡️',
98
- description: '卖家额外质押 DCP 作为质量保证金,问题订单买家可获得额外赔偿。',
98
+ description: '卖家额外质押 WAZ 作为质量保证金,问题订单买家可获得额外赔偿。',
99
99
  description_en: 'Seller stakes extra WAZ as quality bond — buyer gets additional compensation on disputes.',
100
100
  },
101
101
  instant_ship: {
package/dist/ledger.js CHANGED
@@ -1,9 +1,18 @@
1
1
  import { toUnits, toDecimal } from './money.js';
2
2
  /** 读某用户钱包当前余额(整数 base-units)。无钱包行 → 全 0。 */
3
3
  export function walletUnits(db, userId) {
4
- const r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
5
- .get(userId);
6
- return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0) };
4
+ // fee_staked Direct Pay 新增列。向后兼容缺该列的库(迁移前 / 测试用最小 schema):
5
+ // 先按含 fee_staked 查;若该列不存在(no such column)→ 退回 4 列查,fee_staked=0。
6
+ let r;
7
+ try {
8
+ r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned, COALESCE(fee_staked,0) fee_staked FROM wallets WHERE user_id = ?')
9
+ .get(userId);
10
+ }
11
+ catch {
12
+ r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
13
+ .get(userId);
14
+ }
15
+ return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0), fee_staked: toUnits(r?.fee_staked ?? 0) };
7
16
  }
8
17
  /**
9
18
  * 对钱包字段施加整数单位 delta,以【绝对值】落库(读当前→加 delta→写 toDecimal)。
package/dist/mcp.js CHANGED
@@ -4,12 +4,31 @@
4
4
  *
5
5
  * 运行:npm run mcp(或 npx @seasonkoh/webaz);通过 stdio 与 Claude 等 MCP 客户端通信。
6
6
  * 工具实现都在 src/layer1-agent/L1-1-mcp-server/server.ts(本文件只是 bootstrap)。
7
- * 双模:配 WEBAZ_API_KEY → NETWORK( webaz.xyz 共享网络);否则 SANDBOX(本机库)。详见 RFC-003。
7
+ * 模式:配 WEBAZ_API_KEY → NETWORK(可交易,调 webaz.xyz 共享网络);无 key → NETWORK 只读(公共读走 webaz.xyz);
8
+ * 仅 WEBAZ_MODE=sandbox 才用本机库(与全网隔离,dev/demo)。默认【不是】sandbox。详见 RFC-003。
8
9
  *
9
- * 关联 / Related: AGENTS.md(项目地图) · RFC-003(双模) · RFC-004(webaz_feedback)
10
+ * 关联 / Related: AGENTS.md(项目地图) · RFC-003(三态:network / network_readonly / sandbox) · RFC-004(webaz_feedback)
10
11
  */
11
- import { startMCPServer } from './layer1-agent/L1-1-mcp-server/server.js';
12
- startMCPServer().catch((err) => {
12
+ // ⚠️ 只【静态】import cli.js(纯:version + network-mode,无副作用)。【不】静态 import server.js ——
13
+ // server.js 顶层会 initDatabase()/建 schema,静态 import 会让 --version/--help/--mode 也触发 DB 副作用
14
+ // (写 ~/.webaz/*.db)+ 加载整个 server(#186 审计 P1)。故 server.js 只在真正启动时【动态】import。
15
+ import { cliQuickResponse, runDoctor } from './layer1-agent/L1-1-mcp-server/cli.js';
16
+ async function main() {
17
+ const argv = process.argv.slice(2);
18
+ const quick = cliQuickResponse(argv, process.env); // --version / --help / --mode(纯,不碰 DB/server)
19
+ if (quick !== null) {
20
+ console.log(quick);
21
+ process.exit(0);
22
+ }
23
+ if (argv.includes('--doctor')) {
24
+ console.log(await runDoctor(process.env));
25
+ process.exit(0);
26
+ }
27
+ // 只有真正启动 stdio server 时才动态加载 server.js(此时才允许 DB 初始化等副作用)。
28
+ const { startMCPServer } = await import('./layer1-agent/L1-1-mcp-server/server.js');
29
+ await startMCPServer();
30
+ }
31
+ main().catch((err) => {
13
32
  console.error('MCP Server 启动失败:', err);
14
33
  process.exit(1);
15
34
  });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Merchant Base-Bond (v1 collateral-only) — domain skeleton (PR1, testnet/dev scaffold).
3
+ *
4
+ * 设计真相源:docs/modules/MERCHANT-BASE-BOND-DESIGN.INTERNAL.md(v1 candidate-locked)。
5
+ * 本模块是【纯域逻辑骨架】:状态机 + 资格派生 + 类型。**不写 DB、不接任何 live 路径、不碰资金。**
6
+ *
7
+ * v1 已锁(见设计稿 §12):
8
+ * - 链上非托管锁仓合约持有 collateral;买家货款永不进合约;平台费【不上链】(链下账务)。
9
+ * - slash 仅治理触发 + 链上硬约束(v1 无自动/小额 slash)。
10
+ * - 合约 v1 不可升级;链 = Base + 白名单 USDC(单链);入门门 = 单一 base_bond_min_units(不分档)。
11
+ * - wallet rotation = seller 签名 + WebAZ 授权;治理仅紧急。
12
+ * - 计价用 base_bond_min_units(USDC 整数单位),不引预言机。
13
+ *
14
+ * ⚠️ MERCHANT_BOND_V1_ENABLED 默认 false:整套 v1 在 testnet/dev scaffold 阶段【关闭】,
15
+ * mainnet + 真实 USDC 需先过 法务 + 外部合约审计 + Holden 批准(设计稿 §9)。
16
+ */
17
+ /** 全局开关:v1 默认关闭(PR1 scaffold)。真实启用前永远 false。 */
18
+ export const MERCHANT_BOND_V1_ENABLED = false;
19
+ /** v1 链:Base + 白名单 USDC(单链;仅 chain profile 占位,无多链抽象)。 */
20
+ export const MERCHANT_BOND_CHAIN_PROFILE = {
21
+ key: 'base',
22
+ // 真实 chainId / USDC 地址 / 确认数 N 在 mainnet 接入时由治理/配置定稿(政策参数);此处仅占位。
23
+ chainId: null,
24
+ usdcAddress: null,
25
+ minConfirmations: null,
26
+ };
27
+ /** Seller 生命周期状态(枚举;`paused` 不是 seller 状态,pause 是正交 flags)。 */
28
+ export const BOND_STATUS = {
29
+ NONE: 'none',
30
+ PENDING_CONFIRMATIONS: 'pending_confirmations',
31
+ ACTIVE: 'active',
32
+ COOLING: 'cooling',
33
+ WITHDRAWABLE: 'withdrawable',
34
+ WITHDRAWN: 'withdrawn',
35
+ SLASHED_BELOW_MIN: 'slashed_below_min',
36
+ };
37
+ /** 合法状态转移(域层薄校验;真相仍以链上合约 event 为准)。 */
38
+ const VALID_TRANSITIONS = {
39
+ [BOND_STATUS.NONE]: [BOND_STATUS.PENDING_CONFIRMATIONS],
40
+ [BOND_STATUS.PENDING_CONFIRMATIONS]: [BOND_STATUS.ACTIVE, BOND_STATUS.NONE],
41
+ [BOND_STATUS.ACTIVE]: [BOND_STATUS.COOLING, BOND_STATUS.SLASHED_BELOW_MIN],
42
+ [BOND_STATUS.COOLING]: [BOND_STATUS.WITHDRAWABLE, BOND_STATUS.ACTIVE],
43
+ [BOND_STATUS.WITHDRAWABLE]: [BOND_STATUS.WITHDRAWN, BOND_STATUS.ACTIVE],
44
+ [BOND_STATUS.WITHDRAWN]: [],
45
+ [BOND_STATUS.SLASHED_BELOW_MIN]: [BOND_STATUS.ACTIVE],
46
+ };
47
+ export function canTransitionBond(from, to) {
48
+ return (VALID_TRANSITIONS[from] || []).includes(to);
49
+ }
50
+ /**
51
+ * 资格派生(单一真相函数:readiness 与 create gate 必须都调它,定义逐字一致)。
52
+ * fail-closed:开关关 / 未绑钱包 / 未确认 / 状态非 active / 担保不足 → false。
53
+ * v1 入门门 = 单一 base_bond_min_units(不分档)。
54
+ */
55
+ export function deriveSellerHasProductionBaseBondLocked(view, baseBondMinUnits) {
56
+ if (!MERCHANT_BOND_V1_ENABLED)
57
+ return false; // v1 scaffold 默认关 → 永远不翻真实资格
58
+ if (!view)
59
+ return false;
60
+ return view.walletBound
61
+ && view.confirmed
62
+ && view.status === BOND_STATUS.ACTIVE
63
+ && view.collateralUnits >= baseBondMinUnits;
64
+ }
@@ -0,0 +1,78 @@
1
+ import { toUnits } from './money.js';
2
+ export const EXPOSURE_FACTOR_BPS_PARAM = 'direct_pay.exposure_factor_bps';
3
+ export const EXPOSURE_FACTOR_BPS_DEFAULT = 8000;
4
+ export class ExposureCapConfigError extends Error {
5
+ }
6
+ /** fail-closed 读取 factor:整数 ∈ [1,10000];缺失/非数/越界 → 抛 ExposureCapConfigError。 */
7
+ export function readExposureFactorBps(getProtocolParam) {
8
+ const raw = getProtocolParam(EXPOSURE_FACTOR_BPS_PARAM, undefined);
9
+ const n = Number(raw);
10
+ if (raw === undefined || raw === null || raw === '' || !Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 10000) {
11
+ throw new ExposureCapConfigError(`${EXPOSURE_FACTOR_BPS_PARAM} invalid (got ${JSON.stringify(raw)}); must be integer in [1,10000]`);
12
+ }
13
+ return n;
14
+ }
15
+ /** 卖家当前有效抵押(USDC integer units,big-int 安全):merchant_bond_deposits status='active' 之和。无 → 0n。 */
16
+ export function getActiveCollateralUnits(db, sellerId) {
17
+ let rows = [];
18
+ // 表缺失(旧/最小库)→ 视作无抵押(0n),休眠安全:不让缺表破坏现有直付建单。
19
+ try {
20
+ rows = db.prepare("SELECT collateral_units FROM merchant_bond_deposits WHERE seller_id = ? AND status = 'active'").all(sellerId);
21
+ }
22
+ catch {
23
+ return 0n;
24
+ }
25
+ let sum = 0n;
26
+ for (const r of rows) {
27
+ try {
28
+ sum += BigInt(r.collateral_units || '0');
29
+ }
30
+ catch { /* 坏行跳过,不计入(fail-closed:少算抵押更保守) */ }
31
+ }
32
+ return sum;
33
+ }
34
+ /**
35
+ * 开放敞口状态(未完全关闭的 direct_p2p 单)。终态【不】计入(释放敞口):
36
+ * completed / cancelled / fault_* / resolved_for_seller / refunded_partial / refunded_full / dispute_dismissed / expired。
37
+ * ⚠️ direct_expired_unconfirmed 是【非终态】(超时不静默关单,仍可转 disputed / cancelled,见 transitions.ts)→ 必须计入,
38
+ * 漏算会低估敞口(对风险闸是危险方向)。created 为建单原子前态(direct_p2p 一般即转 window),防御性纳入(过度计入只会更严、无害)。
39
+ */
40
+ export const OPEN_EXPOSURE_STATUSES = [
41
+ 'created', 'direct_pay_window', 'direct_expired_unconfirmed',
42
+ 'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'confirmed', 'disputed',
43
+ ];
44
+ /** 卖家当前 Direct Pay 开放敞口(units):未完全关闭的 direct_p2p 单 total_amount 之和。 */
45
+ export function computeDirectPayOpenExposureUnits(db, sellerId) {
46
+ const placeholders = OPEN_EXPOSURE_STATUSES.map(() => '?').join(',');
47
+ const rows = db.prepare(`SELECT total_amount FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${placeholders})`).all(sellerId, ...OPEN_EXPOSURE_STATUSES);
48
+ let sum = 0n;
49
+ for (const r of rows)
50
+ sum += BigInt(toUnits(Number(r.total_amount) || 0));
51
+ return sum;
52
+ }
53
+ /** 纯函数:本单是否在敞口上限内。allowed = collateral × bps / 10000;open + new ≤ allowed。 */
54
+ export function withinExposureCap(args) {
55
+ const allowed = args.activeCollateralUnits * BigInt(args.factorBps) / 10000n;
56
+ return args.openExposureUnits + args.newOrderUnits <= allowed;
57
+ }
58
+ /**
59
+ * create-gate 主入口。休眠安全:collateral==0 → ok(N/A,缓交路径由 deferral quota 管,不读 factor)。
60
+ * collateral>0 → 读 factor(fail-closed)+ 比较;超出 → 拒(EXPOSURE_CAP_EXCEEDED)。
61
+ */
62
+ export function enforceCollateralExposureGate(db, sellerId, newOrderUnits, getProtocolParam) {
63
+ const collateral = getActiveCollateralUnits(db, sellerId);
64
+ if (collateral <= 0n)
65
+ return { ok: true }; // 无真实抵押 → 敞口上限不适用(休眠)
66
+ let factorBps;
67
+ try {
68
+ factorBps = readExposureFactorBps(getProtocolParam);
69
+ }
70
+ catch (e) {
71
+ return { ok: false, error_code: 'EXPOSURE_CAP_CONFIG', reason: e.message };
72
+ } // fail-closed
73
+ const openExposureUnits = computeDirectPayOpenExposureUnits(db, sellerId);
74
+ if (!withinExposureCap({ activeCollateralUnits: collateral, openExposureUnits, newOrderUnits: BigInt(newOrderUnits), factorBps })) {
75
+ return { ok: false, error_code: 'EXPOSURE_CAP_EXCEEDED', reason: 'direct-pay open exposure exceeds collateral-backed cap' };
76
+ }
77
+ return { ok: true };
78
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Merchant Base-Bond — chain watcher skeleton (PR1, testnet/dev scaffold).
3
+ *
4
+ * 设计:docs/modules/MERCHANT-BASE-BOND-DESIGN.INTERNAL.md §4.1/§4.2。
5
+ * 职责(未来 testnet 才接真实 RPC):监听合约 deposit/slash/withdraw event → ≥N confirmations 后
6
+ * 更新 DB 镜像(merchant_bond_*)。reorg 后冻结派生资格 + 告警。
7
+ *
8
+ * ⚠️ PR1 阶段:**纯骨架、默认关、不连任何 RPC、不写资金、不接 live 路径。** 启用前需:
9
+ * chain profile 定稿 + 法务 + 外部合约审计 + Holden 批准(设计稿 §9)。
10
+ */
11
+ import { MERCHANT_BOND_V1_ENABLED, MERCHANT_BOND_CHAIN_PROFILE } from './merchant-bond-domain.js';
12
+ /**
13
+ * 启动 watcher。**PR1:若未启用(默认)或 chain profile 未定稿 → 直接返回 no-op,绝不连 RPC。**
14
+ * 真实事件处理(deposit recognition / confirmations / reorg freeze)留待后续 testnet PR。
15
+ */
16
+ export function startMerchantBondWatcher(_deps = {}) {
17
+ const profileReady = MERCHANT_BOND_CHAIN_PROFILE.chainId != null
18
+ && MERCHANT_BOND_CHAIN_PROFILE.usdcAddress != null
19
+ && MERCHANT_BOND_CHAIN_PROFILE.minConfirmations != null;
20
+ if (!MERCHANT_BOND_V1_ENABLED || !profileReady) {
21
+ // disabled scaffold:no RPC、no timer、no DB write。
22
+ return { enabled: false, stop: () => { } };
23
+ }
24
+ // 真实监听实现 = 后续 testnet PR(deposit/slash/withdraw event → ≥N confirmations → DB 镜像;reorg 冻结+告警)。
25
+ throw new Error('MerchantBondWatcher: real chain listening not implemented in PR1 scaffold');
26
+ }
@@ -0,0 +1,29 @@
1
+ import { takeFeeAtCompletion, releaseFeeStake, slashFeeStakeToPenalty } from './direct-pay-ledger.js';
2
+ /**
3
+ * Rail 1 = 非托管场外直付 + 信誉(buyer protection = reputation-only, refund = none)。
4
+ * ⚠️ 本 seam 当前【未接线】(getPaymentRail 无调用方)。Rail1 平台费【实时路径】= 首单宽限 + 预充值续用:
5
+ * 建单走【首单宽限 + 预充值门】(direct-pay-create.ts),完成时 accrue 应收(server.ts settleOrder direct_p2p 分支)。
6
+ * 下方 collectFeeAtCompletion/release/slash 仍指向旧 WAZ fee-stake helper = 遗留/forward-compat 占位;
7
+ * 若将来真接线本 seam,须改为应收/预充值语义(accrue / prepay)。
8
+ */
9
+ export const RAIL_DIRECT_P2P = {
10
+ contract: {
11
+ id: 'direct_p2p',
12
+ buyerProtection: 'reputation_only',
13
+ refund: 'none',
14
+ disputeRemedyBasis: 'evidence_reputation',
15
+ custodialOfTradeFunds: false,
16
+ },
17
+ collectFeeAtCompletion: (db, orderId) => takeFeeAtCompletion(db, { orderId }),
18
+ releaseOnExpiryOrCancel: (db, orderId) => releaseFeeStake(db, { orderId }),
19
+ slashOnFault: (db, orderId, txnId, reason) => slashFeeStakeToPenalty(db, { orderId, txnId, reason }),
20
+ };
21
+ /**
22
+ * 取轨道实现。只 Rail 1 已实现;'escrow' 走既有系统(不经此抽象);'onchain_full_stake' / 'psp' = GATED(未实现)。
23
+ * 故意用显式 switch,不建插件注册框架(anti-YAGNI)。
24
+ */
25
+ export function getPaymentRail(railId) {
26
+ if (railId === 'direct_p2p')
27
+ return RAIL_DIRECT_P2P;
28
+ return null; // escrow=既有系统; onchain_full_stake/psp=未实现(forward-compat 契约见上)
29
+ }
@@ -0,0 +1,94 @@
1
+ import { validateQrDataUri } from './direct-receive-account-qr.js';
2
+ import { MAX_INSTRUCTION_LEN, MAX_LABEL_LEN, MAX_METHOD_LEN, CURRENCY_RE } from './direct-receive-accounts.js';
3
+ /** PURE:校验 + 规范化文本字段(trim / 长度 / 币种)。长度超限=显式拒(非静默截断)。 */
4
+ export function normalizePlatformText(input) {
5
+ const instruction = String(input?.instruction ?? '').trim();
6
+ if (!instruction)
7
+ return { ok: false, reason: 'instruction required' };
8
+ if (instruction.length > MAX_INSTRUCTION_LEN)
9
+ return { ok: false, reason: `instruction must be ≤ ${MAX_INSTRUCTION_LEN} chars` };
10
+ const field = (v, max, name) => {
11
+ if (v == null)
12
+ return { ok: true, v: null };
13
+ const s = String(v).trim();
14
+ if (s.length > max)
15
+ return { ok: false, reason: `${name} must be ≤ ${max} chars` };
16
+ return { ok: true, v: s || null };
17
+ };
18
+ const L = field(input.label, MAX_LABEL_LEN, 'label');
19
+ if (!L.ok)
20
+ return L;
21
+ const M = field(input.method, MAX_METHOD_LEN, 'method');
22
+ if (!M.ok)
23
+ return M;
24
+ let currency = null;
25
+ if (input.currency != null && String(input.currency).trim()) {
26
+ currency = String(input.currency).trim().toUpperCase();
27
+ if (!CURRENCY_RE.test(currency))
28
+ return { ok: false, reason: 'currency must be a 2-8 char code (e.g. SGD, USDC)' };
29
+ }
30
+ return { ok: true, value: { instruction, label: L.v, method: M.v, currency } };
31
+ }
32
+ /** PURE:解析 qrDataUri 入参 → 'keep'(缺省不改) | null(清除) | 校验后的 data-uri | 错误。 */
33
+ export function resolveQrInput(input) {
34
+ if (!('qrDataUri' in input) || input.qrDataUri === undefined)
35
+ return { ok: true, qr: 'keep' };
36
+ const q = input.qrDataUri;
37
+ if (q == null || String(q).trim() === '')
38
+ return { ok: true, qr: null }; // 清除
39
+ const v = validateQrDataUri(q);
40
+ if (!v.ok)
41
+ return { ok: false, reason: v.reason };
42
+ return { ok: true, qr: String(q).trim() };
43
+ }
44
+ const COLS = 'id, label, method, currency, instruction, qr_data_uri, status';
45
+ /** 全部平台收款方式(默认仅 active;{ includeInactive:true } 取全部,含 qr_data_uri)。admin 用。 */
46
+ export function listPlatformAccounts(db, opts = {}) {
47
+ const where = opts.includeInactive ? '' : " WHERE status = 'active'";
48
+ return db.prepare(`SELECT ${COLS} FROM platform_receive_accounts${where} ORDER BY created_at ASC, id ASC`).all();
49
+ }
50
+ /** 仅 active(卖家侧看:含 instruction + qr_data_uri —— 平台公开收款明细,非披露门)。 */
51
+ export function listActivePlatformAccounts(db) {
52
+ return listPlatformAccounts(db, { includeInactive: false });
53
+ }
54
+ export function getPlatformAccount(db, id) {
55
+ return db.prepare(`SELECT ${COLS} FROM platform_receive_accounts WHERE id = ?`).get(id) ?? null;
56
+ }
57
+ /** 新增 active 平台收款方式。 */
58
+ export function addPlatformAccount(db, input, generateId) {
59
+ const t = normalizePlatformText(input);
60
+ if (!t.ok)
61
+ return t;
62
+ const q = resolveQrInput(input);
63
+ if (!q.ok)
64
+ return q;
65
+ const qr = q.qr === 'keep' ? null : q.qr; // 新增时无既有值,'keep' 视作无 qr
66
+ const id = generateId('pra');
67
+ db.prepare(`INSERT INTO platform_receive_accounts (id, label, method, currency, instruction, qr_data_uri, status) VALUES (?,?,?,?,?,?, 'active')`)
68
+ .run(id, t.value.label, t.value.method, t.value.currency, t.value.instruction, qr);
69
+ return { ok: true, account: { id, status: 'active', qr_data_uri: qr, ...t.value } };
70
+ }
71
+ /** 更新(文本总更;qr 仅当入参给了 qrDataUri 才动 —— 缺省保留既有)。 */
72
+ export function updatePlatformAccount(db, id, input) {
73
+ const t = normalizePlatformText(input);
74
+ if (!t.ok)
75
+ return t;
76
+ const q = resolveQrInput(input);
77
+ if (!q.ok)
78
+ return q;
79
+ const v = t.value;
80
+ let info;
81
+ if (q.qr === 'keep') {
82
+ info = db.prepare(`UPDATE platform_receive_accounts SET label=?, method=?, currency=?, instruction=?, updated_at=datetime('now') WHERE id=?`)
83
+ .run(v.label, v.method, v.currency, v.instruction, id);
84
+ }
85
+ else {
86
+ info = db.prepare(`UPDATE platform_receive_accounts SET label=?, method=?, currency=?, instruction=?, qr_data_uri=?, updated_at=datetime('now') WHERE id=?`)
87
+ .run(v.label, v.method, v.currency, v.instruction, q.qr, id);
88
+ }
89
+ return { ok: true, changed: info.changes > 0 };
90
+ }
91
+ /** 软停用(卖家不再看到)。 */
92
+ export function deactivatePlatformAccount(db, id) {
93
+ return db.prepare("UPDATE platform_receive_accounts SET status='inactive', updated_at=datetime('now') WHERE id=? AND status='active'").run(id).changes > 0;
94
+ }
@@ -0,0 +1,93 @@
1
+ export const MAX_URL_LEN = 2048;
2
+ export const MAX_PLATFORM_LEN = 60;
3
+ export const MAX_NOTES_LEN = 500;
4
+ const ACTIVE = "('issued','submitted','verified')";
5
+ const getActiveForProduct = (db, productId) => db.prepare(`SELECT * FROM product_verifications WHERE product_id = ? AND status IN ${ACTIVE} ORDER BY created_at DESC, rowid DESC LIMIT 1`).get(productId);
6
+ /** http(s) 链接最弱校验(仅存储,不抓取):必须 http/https、长度受限。拒 javascript:/data: 等危险 scheme。 */
7
+ function isStorableHttpUrl(url) {
8
+ if (typeof url !== 'string' || url.length === 0 || url.length > MAX_URL_LEN)
9
+ return false;
10
+ return /^https?:\/\/[^\s]+$/i.test(url.trim());
11
+ }
12
+ /** 卖家为【某产品】申请认证 → issued(签发 code)。绝不自动 verify。单一活跃(per product)。调用方须先校验 seller 拥有该产品。 */
13
+ export function requestProductVerification(db, args) {
14
+ const { id, productId, sellerId, code } = args;
15
+ if (!id || !productId || !sellerId || !code)
16
+ return { ok: false, reason: 'missing id/productId/sellerId/code' };
17
+ const platform = args.platform ? String(args.platform).trim().slice(0, MAX_PLATFORM_LEN) : null;
18
+ const active = getActiveForProduct(db, productId);
19
+ if (active)
20
+ return { ok: false, reason: `该产品已有进行中的认证(${active.status})` };
21
+ db.prepare(`INSERT INTO product_verifications (id, product_id, seller_id, code, platform, status, created_at, updated_at)
22
+ VALUES (?,?,?,?,?, 'issued', datetime('now'), datetime('now'))`).run(id, productId, sellerId, code, platform);
23
+ return { ok: true, status: 'issued', code };
24
+ }
25
+ /** 卖家为【某产品】提交外部链接 → submitted。要求该产品有 issued 记录;链接仅存储(不抓取)。调用方须校验 seller 拥有该产品。 */
26
+ export function submitProductVerificationLink(db, args) {
27
+ const url = String(args.externalUrl || '').trim();
28
+ if (!isStorableHttpUrl(url))
29
+ return { ok: false, reason: '请提交有效的 http(s) 商品链接' };
30
+ const active = getActiveForProduct(db, args.productId);
31
+ if (!active)
32
+ return { ok: false, reason: '请先为该产品申请认证获取验证码' };
33
+ if (active.status === 'verified')
34
+ return { ok: false, reason: '该产品认证已通过,无需重复提交' };
35
+ const platform = args.platform != null ? String(args.platform).trim().slice(0, MAX_PLATFORM_LEN) : active.platform;
36
+ db.prepare(`UPDATE product_verifications SET external_url = ?, platform = ?, status = 'submitted', updated_at = datetime('now') WHERE id = ?`)
37
+ .run(url, platform, active.id);
38
+ return { ok: true, status: 'submitted' };
39
+ }
40
+ /** 真人 admin 手动核对结论 → verified | rejected。仅从 submitted 流转。reviewerId 必填(责任分层;ROOT/Passkey 由调用方强制)。 */
41
+ export function reviewProductVerification(db, args) {
42
+ const { id, reviewerId, decision } = args;
43
+ if (!reviewerId)
44
+ return { ok: false, reason: 'reviewProductVerification requires a human reviewerId' };
45
+ if (decision !== 'verified' && decision !== 'rejected')
46
+ return { ok: false, reason: 'decision must be verified|rejected' };
47
+ const row = db.prepare('SELECT * FROM product_verifications WHERE id = ?').get(id);
48
+ if (!row)
49
+ return { ok: false, reason: 'product verification not found' };
50
+ if (row.status === decision)
51
+ return { ok: true, status: decision, already: true };
52
+ if (row.status !== 'submitted')
53
+ return { ok: false, reason: `cannot review from status '${row.status}' (need submitted)` };
54
+ const notes = args.notes != null ? String(args.notes).slice(0, MAX_NOTES_LEN) : null;
55
+ db.prepare(`UPDATE product_verifications SET status = ?, reviewed_by = ?, reviewed_at = datetime('now'), notes = ?, updated_at = datetime('now') WHERE id = ?`)
56
+ .run(decision, reviewerId, notes, id);
57
+ return { ok: true, status: decision };
58
+ }
59
+ /** 某产品最新一条认证记录(任意状态)。无 → null。 */
60
+ export function getProductVerification(db, productId) {
61
+ return db.prepare('SELECT * FROM product_verifications WHERE product_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1').get(productId) ?? null;
62
+ }
63
+ /** 某卖家所有产品认证记录(供卖家自助面板逐产品展示状态)。最新在前。 */
64
+ export function listSellerProductVerifications(db, sellerId) {
65
+ return db.prepare('SELECT * FROM product_verifications WHERE seller_id = ? ORDER BY created_at DESC, rowid DESC').all(sellerId);
66
+ }
67
+ export function toSellerProductVerificationView(row) {
68
+ return {
69
+ id: row.id, product_id: row.product_id, code: row.code, platform: row.platform, external_url: row.external_url,
70
+ status: row.status, reviewed_at: row.reviewed_at, created_at: row.created_at, updated_at: row.updated_at,
71
+ }; // 故意省略 reviewed_by + notes(admin 身份 / 内部备注)
72
+ }
73
+ /** admin 队列:按 status 过滤(默认全部),最新在前。纯读。 */
74
+ export function listProductVerifications(db, opts = {}) {
75
+ if (opts.status)
76
+ return db.prepare(`SELECT * FROM product_verifications WHERE status = ? ORDER BY created_at DESC, rowid DESC`).all(opts.status);
77
+ return db.prepare(`SELECT * FROM product_verifications ORDER BY created_at DESC, rowid DESC`).all();
78
+ }
79
+ /** 硬门读取:该产品是否【已验证】(有 verified 记录)。供 direct-pay create/availability 强制(未验证 → 不可直付)。 */
80
+ export function productStoreVerified(db, productId) {
81
+ return !!db.prepare("SELECT 1 FROM product_verifications WHERE product_id = ? AND status = 'verified' LIMIT 1").get(productId);
82
+ }
83
+ /**
84
+ * 反作弊生命周期:商品发生【重大编辑】(标题/价格/详情/外部链接)时作废其验证 → status='stale'。
85
+ * 之后 productStoreVerified=false(硬门重新拦截),且 active 记录被清(卖家可重新申领→提交→admin 重核)。
86
+ * 防"先把商品 A 验证通过,再改标题/价格/详情/链接变成商品 B"绕过逐品门。返回作废条数。纯写,只改本表。
87
+ * 注:store-level 豁免卖家不依赖逐品验证,本作废对其直付资格无影响。
88
+ */
89
+ export function invalidateProductVerification(db, productId) {
90
+ const r = db.prepare(`UPDATE product_verifications SET status = 'stale', updated_at = datetime('now')
91
+ WHERE product_id = ? AND status IN ('issued','submitted','verified')`).run(productId);
92
+ return { invalidated: r.changes };
93
+ }