@seasonkoh/webaz 0.1.7 → 0.1.9

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 (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31947 -0
  22. package/dist/pwa/public/i18n.js +5751 -0
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +21 -0
  25. package/dist/pwa/public/manifest.json +48 -0
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +535 -0
  28. package/dist/pwa/public/sw.js +63 -0
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9679 -698
  153. package/package.json +11 -4
@@ -0,0 +1,475 @@
1
+ import express from 'express';
2
+ export function registerDisputesWriteRoutes(app, deps) {
3
+ const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend } = deps;
4
+ // 被诉方反驳
5
+ app.post('/api/disputes/:id/respond', (req, res) => {
6
+ const user = auth(req, res);
7
+ if (!user)
8
+ return;
9
+ const { notes = '', evidence_description = '' } = req.body;
10
+ const dispute = getDisputeDetails(db, req.params.id);
11
+ if (!dispute)
12
+ return void res.status(404).json({ error: '争议不存在' });
13
+ if (dispute.defendant_id !== user.id)
14
+ return void res.status(403).json({ error: '你不是本争议的被诉方' });
15
+ const evidenceIds = [];
16
+ if (evidence_description) {
17
+ const eid = generateId('evt');
18
+ const evReasons = detectFraud(String(evidence_description));
19
+ db.prepare(`INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash, flag_reasons)
20
+ VALUES (?,?,?,'description',?,?,?)`).run(eid, dispute.order_id, user.id, evidence_description, `hash_${Date.now()}`, evReasons.length ? JSON.stringify(evReasons) : null);
21
+ evidenceIds.push(eid);
22
+ }
23
+ const result = respondToDispute(db, req.params.id, user.id, notes || evidence_description, evidenceIds);
24
+ if (!result.success)
25
+ return void res.json({ error: result.error });
26
+ res.json({ success: true, message: result.message });
27
+ });
28
+ // 仲裁员裁定
29
+ app.post('/api/disputes/:id/arbitrate', (req, res) => {
30
+ const user = auth(req, res);
31
+ if (!user)
32
+ return;
33
+ const elig = isEligibleArbitrator(user.id);
34
+ if (!elig.ok)
35
+ return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
36
+ // 2026-05-23 Agent 治理铁律:仲裁需真实人工
37
+ const hpCheck = requireHumanPresence(user.id, 'arbitrate', req.body?.webauthn_token, 'require_human_presence_for_arbitrate', (data) => {
38
+ const d = data;
39
+ return d == null || d.dispute_id === req.params.id;
40
+ });
41
+ if (!hpCheck.ok)
42
+ return void errorRes(res, 412, hpCheck.error_code || 'HUMAN_PRESENCE_REQUIRED', hpCheck.reason || '此操作需真实人工 WebAuthn 验证');
43
+ const { ruling, reason, refund_amount, liable_party_id, liability_parties } = req.body;
44
+ if (!ruling || !reason)
45
+ return void res.json({ error: '请提供裁定结果(ruling)和理由(reason)' });
46
+ const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split'];
47
+ if (!validRulings.includes(ruling)) {
48
+ return void res.json({ error: `ruling 必须是 ${validRulings.join(' / ')} 之一` });
49
+ }
50
+ if (ruling === 'liability_split') {
51
+ if (!Array.isArray(liability_parties) || liability_parties.length === 0) {
52
+ return void res.json({ error: '责任分配裁定需要提供 liability_parties 数组' });
53
+ }
54
+ for (const p of liability_parties) {
55
+ if (!p.user_id || typeof p.amount !== 'number' || p.amount < 0) {
56
+ return void res.json({ error: '每个责任方需提供 user_id 和非负 amount' });
57
+ }
58
+ }
59
+ }
60
+ const dispute = getDisputeDetails(db, req.params.id);
61
+ if (!dispute)
62
+ return void res.status(404).json({ error: '争议不存在' });
63
+ // P0: 防"任意仲裁员裁决任意争议"
64
+ // 若 assigned_arbitrators 为空 → 首位调用者原子领取
65
+ const arbRow = db.prepare(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`).get(req.params.id);
66
+ let assignedArbitrators = [];
67
+ try {
68
+ assignedArbitrators = JSON.parse(arbRow?.assigned_arbitrators || '[]');
69
+ }
70
+ catch { }
71
+ if (assignedArbitrators.length === 0) {
72
+ const claimRes = db.prepare(`UPDATE disputes SET assigned_arbitrators = ? WHERE id = ? AND (assigned_arbitrators IS NULL OR assigned_arbitrators = '[]')`)
73
+ .run(JSON.stringify([user.id]), req.params.id);
74
+ if (claimRes.changes === 0) {
75
+ const fresh = db.prepare(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`).get(req.params.id);
76
+ try {
77
+ assignedArbitrators = JSON.parse(fresh?.assigned_arbitrators || '[]');
78
+ }
79
+ catch { }
80
+ }
81
+ else {
82
+ assignedArbitrators = [user.id];
83
+ }
84
+ }
85
+ if (!assignedArbitrators.includes(user.id)) {
86
+ return void res.status(403).json({ error: '此争议未分配给你 — 仅指派的仲裁员可裁定', error_code: 'NOT_ASSIGNED_ARBITRATOR' });
87
+ }
88
+ // 协议层:仲裁员签名的 ruling 入订单链
89
+ try {
90
+ appendOrderEvent(db, {
91
+ orderId: dispute.order_id,
92
+ eventType: 'transition',
93
+ fromStatus: 'disputed',
94
+ toStatus: 'disputed',
95
+ actorId: user.id,
96
+ actorRole: 'arbitrator',
97
+ extra: {
98
+ action: 'arbitration_ruling',
99
+ dispute_id: req.params.id,
100
+ ruling, reason,
101
+ refund_amount: refund_amount ? Number(refund_amount) : null,
102
+ liable_party_id: liable_party_id || null,
103
+ liability_parties: liability_parties || null,
104
+ },
105
+ });
106
+ }
107
+ catch (e) {
108
+ console.warn('[order-chain] arbitration ruling event failed:', e.message);
109
+ }
110
+ const result = arbitrateDispute(db, req.params.id, user.id, ruling, reason, refund_amount ? Number(refund_amount) : undefined, liability_parties, liable_party_id);
111
+ if (!result.success)
112
+ return void res.json({ error: result.error });
113
+ // 争议结案 → 给证据 blob 打过期戳
114
+ try {
115
+ markEvidenceExpiry(db, req.params.id);
116
+ }
117
+ catch (e) {
118
+ console.warn('[evidence] mark expiry:', e.message);
119
+ }
120
+ // release_seller 等同正常完成 → 触发推土机分润 + 原子能
121
+ if (ruling === 'release_seller') {
122
+ try {
123
+ db.transaction(() => {
124
+ const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
125
+ if (!order || order.settled_commission_at)
126
+ return;
127
+ const total = Number(order.total_amount);
128
+ const commRate = Number(order.snapshot_commission_rate ?? 0);
129
+ const commPool = Math.round(total * commRate * 100) / 100;
130
+ const fundBase = Math.round(total * FUND_BASE_RATE() * 100) / 100;
131
+ const deduct = commPool + fundBase;
132
+ const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
133
+ if (sellerWallet && sellerWallet.balance >= deduct) {
134
+ db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
135
+ const { redirected: disputeRedirected } = settleCommission(dispute.order_id);
136
+ depositToFund(dispute.order_id, disputeRedirected);
137
+ const productRow = db.prepare("SELECT category_id FROM products WHERE id = ?").get(order.product_id);
138
+ const categoryId = productRow?.category_id || 'cat_default';
139
+ const catRow = db.prepare("SELECT pv_multiplier FROM product_categories WHERE id = ?").get(categoryId);
140
+ const mPv = Number(catRow?.pv_multiplier ?? 1.0);
141
+ const pv = calculatePv(total, mPv);
142
+ if (pv > 0) {
143
+ db.prepare(`INSERT INTO pv_ledger (id, order_id, buyer_id, pv, processed) VALUES (?,?,?,?,0)`)
144
+ .run(generateId('pvl'), dispute.order_id, order.buyer_id, pv);
145
+ db.prepare("UPDATE users SET pv_dirty_at = datetime('now') WHERE id = ?").run(order.buyer_id);
146
+ }
147
+ db.prepare("UPDATE orders SET settled_pv_at = datetime('now') WHERE id = ?").run(dispute.order_id);
148
+ }
149
+ else {
150
+ // P2 #6:seller 余额不足,commission/PV 被吞 — 记 audit
151
+ console.warn(`[dispute hook · release_seller] commission/PV 被吞 order=${dispute.order_id} seller=${order.seller_id} balance=${sellerWallet?.balance ?? 'null'} required=${deduct}`);
152
+ try {
153
+ logAdminAction('system', 'commission_skipped', 'order', String(dispute.order_id), { reason: 'seller_balance_insufficient', ruling: 'release_seller', required: deduct, actual: sellerWallet?.balance ?? 0 });
154
+ }
155
+ catch { }
156
+ }
157
+ })();
158
+ }
159
+ catch (e) {
160
+ console.error('[dispute commission/pv hook]', e);
161
+ }
162
+ }
163
+ // Bug-A fix:partial_refund / liability_split 也按 effectiveBase 发 commission/PV/基金池
164
+ if (ruling === 'partial_refund' || ruling === 'liability_split') {
165
+ try {
166
+ db.transaction(() => {
167
+ const order = db.prepare("SELECT * FROM orders WHERE id = ?").get(dispute.order_id);
168
+ if (!order || order.settled_commission_at)
169
+ return;
170
+ let effectiveBase = 0;
171
+ if (ruling === 'partial_refund' && liable_party_id) {
172
+ effectiveBase = Number(order.total_amount);
173
+ }
174
+ else if (ruling === 'partial_refund') {
175
+ effectiveBase = Number(result.settlement?.seller_received ?? 0);
176
+ }
177
+ else {
178
+ effectiveBase = Number(result.settlement?.seller_escrow_share ?? 0);
179
+ }
180
+ if (effectiveBase <= 0)
181
+ return;
182
+ const commRate = Number(order.snapshot_commission_rate ?? 0);
183
+ const commPool = Math.round(effectiveBase * commRate * 100) / 100;
184
+ const fundBase = Math.round(effectiveBase * FUND_BASE_RATE() * 100) / 100;
185
+ const deduct = Math.round((commPool + fundBase) * 100) / 100;
186
+ const sellerWallet = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(order.seller_id);
187
+ if (sellerWallet && sellerWallet.balance >= deduct) {
188
+ if (deduct > 0) {
189
+ db.prepare("UPDATE wallets SET balance = balance - ?, earned = earned - ? WHERE user_id = ?").run(deduct, deduct, order.seller_id);
190
+ }
191
+ const { redirected } = settleCommission(dispute.order_id, effectiveBase);
192
+ depositToFund(dispute.order_id, redirected, effectiveBase);
193
+ const productRow = db.prepare("SELECT category_id FROM products WHERE id = ?").get(order.product_id);
194
+ const categoryId = productRow?.category_id || 'cat_default';
195
+ const catRow = db.prepare("SELECT pv_multiplier FROM product_categories WHERE id = ?").get(categoryId);
196
+ const mPv = Number(catRow?.pv_multiplier ?? 1.0);
197
+ const pv = calculatePv(effectiveBase, mPv);
198
+ if (pv > 0) {
199
+ db.prepare(`INSERT INTO pv_ledger (id, order_id, buyer_id, pv, processed) VALUES (?,?,?,?,0)`)
200
+ .run(generateId('pvl'), dispute.order_id, order.buyer_id, pv);
201
+ db.prepare("UPDATE users SET pv_dirty_at = datetime('now') WHERE id = ?").run(order.buyer_id);
202
+ }
203
+ db.prepare("UPDATE orders SET settled_pv_at = datetime('now') WHERE id = ?").run(dispute.order_id);
204
+ }
205
+ else {
206
+ // P2 #6:seller 余额不足,commission/PV 被吞
207
+ console.warn(`[dispute hook · ${ruling}] commission/PV 被吞 order=${dispute.order_id} seller=${order.seller_id} balance=${sellerWallet?.balance ?? 'null'} required=${deduct} effectiveBase=${effectiveBase}`);
208
+ try {
209
+ logAdminAction('system', 'commission_skipped', 'order', String(dispute.order_id), { reason: 'seller_balance_insufficient', ruling, required: deduct, actual: sellerWallet?.balance ?? 0, effectiveBase });
210
+ }
211
+ catch { }
212
+ }
213
+ })();
214
+ }
215
+ catch (e) {
216
+ console.error('[partial_refund/liability_split commission/pv hook]', e);
217
+ }
218
+ }
219
+ // 争议声誉更新(责任分配时以主要责任方为败诉方)
220
+ let winnerId = null;
221
+ let loserId = null;
222
+ if (ruling === 'refund_buyer') {
223
+ winnerId = dispute.initiator_id;
224
+ loserId = dispute.defendant_id;
225
+ }
226
+ else if (ruling === 'release_seller') {
227
+ winnerId = dispute.defendant_id;
228
+ loserId = dispute.initiator_id;
229
+ }
230
+ else if (ruling === 'liability_split' && Array.isArray(liability_parties) && liability_parties.length > 0) {
231
+ const maxLiable = liability_parties.reduce((a, b) => a.amount >= b.amount ? a : b);
232
+ loserId = maxLiable.user_id;
233
+ winnerId = dispute.initiator_id !== loserId ? dispute.initiator_id : dispute.defendant_id;
234
+ }
235
+ if (winnerId && loserId)
236
+ recordDisputeReputation(db, dispute.order_id, winnerId, loserId);
237
+ // Tier 7:商品级争议败诉计数(卖家败诉时 +1)
238
+ try {
239
+ const sellerLost = (ruling === 'refund_buyer' || ruling === 'partial_refund'
240
+ || (ruling === 'liability_split' && loserId === dispute.defendant_id));
241
+ if (sellerLost) {
242
+ const orderRow = db.prepare('SELECT product_id FROM orders WHERE id = ?').get(dispute.order_id);
243
+ if (orderRow?.product_id) {
244
+ db.prepare(`UPDATE products SET dispute_loss_count = COALESCE(dispute_loss_count, 0) + 1 WHERE id = ?`).run(orderRow.product_id);
245
+ }
246
+ }
247
+ }
248
+ catch (e) {
249
+ console.error('[Tier7-hook dispute]', e);
250
+ }
251
+ // 2026-05-23 P0: 败诉方若 api_key 行为可能为 agent 代操 → 发 strike
252
+ try {
253
+ if (loserId) {
254
+ const loserKey = db.prepare(`SELECT api_key FROM users WHERE id = ?`).get(loserId);
255
+ if (loserKey?.api_key) {
256
+ issueAgentStrike({
257
+ apiKey: loserKey.api_key,
258
+ userId: loserId,
259
+ reasonCode: 'dispute_loss',
260
+ reasonDetail: `仲裁裁定 ${ruling}`,
261
+ relatedRef: req.params.id,
262
+ });
263
+ }
264
+ }
265
+ }
266
+ catch (e) {
267
+ console.error('[strike issuance]', e);
268
+ }
269
+ // 自动发布到公开判例库(脱敏)
270
+ try {
271
+ publishDisputeCase(req.params.id, ruling, reason);
272
+ }
273
+ catch (e) {
274
+ console.error('[publishDisputeCase]', e);
275
+ }
276
+ res.json({ success: true, message: result.message, settlement: result.settlement });
277
+ });
278
+ // 参与方主动举证(text)+ SNF 信封分发
279
+ app.post('/api/disputes/:id/add-evidence', (req, res) => {
280
+ const user = auth(req, res);
281
+ if (!user)
282
+ return;
283
+ const { description, evidence_type = 'text', file_hash } = req.body;
284
+ if (!description?.trim())
285
+ return void res.json({ error: '请填写证据内容' });
286
+ const rawDesc = String(description).trim();
287
+ const result = addPartyEvidence(db, req.params.id, user.id, rawDesc, evidence_type, file_hash);
288
+ if (!result.success)
289
+ return void res.json({ error: result.error });
290
+ // 跨窗反诈
291
+ const evReasons = detectFraud(rawDesc);
292
+ if (evReasons.length > 0 && result.evidenceId) {
293
+ try {
294
+ db.prepare(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`)
295
+ .run(JSON.stringify(evReasons), result.evidenceId);
296
+ }
297
+ catch { }
298
+ }
299
+ // 协议层:作为签名 SNF 信封投到对方 + 已分配仲裁员 inbox
300
+ try {
301
+ const d = db.prepare(`SELECT order_id, initiator_id, defendant_id, assigned_arbitrators FROM disputes WHERE id = ?`).get(req.params.id);
302
+ if (d) {
303
+ const uid = user.id;
304
+ const recipients = new Set();
305
+ if (d.initiator_id && d.initiator_id !== uid)
306
+ recipients.add(d.initiator_id);
307
+ if (d.defendant_id && d.defendant_id !== uid)
308
+ recipients.add(d.defendant_id);
309
+ try {
310
+ const arbs = JSON.parse(d.assigned_arbitrators || '[]');
311
+ for (const a of arbs)
312
+ if (a && a !== uid)
313
+ recipients.add(a);
314
+ }
315
+ catch { }
316
+ const envelope = {
317
+ dispute_id: req.params.id,
318
+ evidence_id: result.evidenceId,
319
+ anchor_hash: result.anchorHash,
320
+ evidence_type,
321
+ description: description.trim(),
322
+ file_hash: file_hash || null,
323
+ };
324
+ for (const rid of recipients) {
325
+ try {
326
+ snfSend(db, {
327
+ senderId: uid, recipientId: rid,
328
+ messageType: 'dispute_evidence',
329
+ payload: envelope, priority: 1,
330
+ relatedOrderId: d.order_id,
331
+ });
332
+ }
333
+ catch (e) {
334
+ console.warn('[snf dispute-evidence]', rid, e.message);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ catch (e) {
340
+ console.warn('[snf dispute-evidence] route err:', e.message);
341
+ }
342
+ res.json({ success: true, evidence_id: result.evidenceId, anchor_hash: result.anchorHash });
343
+ });
344
+ // L0-4 证据 blob 上传 — raw body + HMAC 签 + dedup + SNF 分发
345
+ // M: 轻量 authGuard 在 raw 解析之前 — 挡掉 unauth 20MB 请求避免内存浪费
346
+ const lightAuthGuard = (req, res, next) => {
347
+ const hasAuth = !!req.headers.authorization;
348
+ if (!hasAuth)
349
+ return void res.status(401).json({ error: 'auth required' });
350
+ next();
351
+ };
352
+ // N: limit 精确 = EVIDENCE_MAX_BYTES
353
+ app.post('/api/disputes/:id/evidence-blob', lightAuthGuard, express.raw({ type: 'application/octet-stream', limit: EVIDENCE_MAX_BYTES }), (req, res) => {
354
+ const user = auth(req, res);
355
+ if (!user)
356
+ return;
357
+ const hash = String(req.headers['x-content-hash'] || '').trim().toLowerCase();
358
+ const mime = String(req.headers['x-content-mime'] || '').trim().toLowerCase();
359
+ // J: decodeURIComponent 收 %ZZ 坏序列会抛 URIError → 400
360
+ let description;
361
+ let filename;
362
+ try {
363
+ description = decodeURIComponent(String(req.headers['x-description'] || '').trim());
364
+ filename = req.headers['x-filename'] ? decodeURIComponent(String(req.headers['x-filename'])) : undefined;
365
+ }
366
+ catch {
367
+ return void res.status(400).json({ error: 'malformed_header_encoding' });
368
+ }
369
+ // L: filename 长度封顶
370
+ if (filename && filename.length > 200)
371
+ return void res.status(400).json({ error: 'filename_too_long' });
372
+ if (!hash || !/^[0-9a-f]{64}$/.test(hash))
373
+ return void res.status(400).json({ error: 'invalid_hash' });
374
+ if (!mime)
375
+ return void res.status(400).json({ error: 'missing_mime' });
376
+ if (!EVIDENCE_ALLOWED_MIME.has(mime))
377
+ return void res.status(415).json({ error: 'mime_not_allowed', allowed: [...EVIDENCE_ALLOWED_MIME] });
378
+ const blob = req.body;
379
+ if (!Buffer.isBuffer(blob) || blob.length === 0)
380
+ return void res.status(400).json({ error: 'empty_body' });
381
+ try {
382
+ const out = uploadEvidence(db, {
383
+ uploaderId: user.id,
384
+ uploaderApiKey: user.api_key,
385
+ disputeId: String(req.params.id),
386
+ blob, declaredHash: hash, mime, description, filename,
387
+ });
388
+ // 跨窗反诈:detect description(已 decoded)
389
+ const evReasons = detectFraud(description);
390
+ if (evReasons.length > 0 && out.id) {
391
+ try {
392
+ db.prepare(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`)
393
+ .run(JSON.stringify(evReasons), out.id);
394
+ }
395
+ catch { }
396
+ }
397
+ // SNF 信封投递
398
+ try {
399
+ const d = db.prepare(`SELECT order_id, initiator_id, defendant_id, assigned_arbitrators FROM disputes WHERE id = ?`)
400
+ .get(req.params.id);
401
+ if (d) {
402
+ const uid = user.id;
403
+ const recipients = new Set();
404
+ if (d.initiator_id && d.initiator_id !== uid)
405
+ recipients.add(d.initiator_id);
406
+ if (d.defendant_id && d.defendant_id !== uid)
407
+ recipients.add(d.defendant_id);
408
+ try {
409
+ const arbs = JSON.parse(d.assigned_arbitrators || '[]');
410
+ for (const a of arbs)
411
+ if (a && a !== uid)
412
+ recipients.add(a);
413
+ }
414
+ catch { }
415
+ for (const rid of recipients) {
416
+ try {
417
+ snfSend(db, {
418
+ senderId: uid, recipientId: rid,
419
+ messageType: 'dispute_evidence_blob',
420
+ payload: {
421
+ dispute_id: req.params.id,
422
+ evidence_id: out.id,
423
+ file_hash: out.hash,
424
+ size: out.size,
425
+ mime, description, filename: filename || null,
426
+ sig: out.sig,
427
+ },
428
+ priority: 1, relatedOrderId: d.order_id,
429
+ });
430
+ }
431
+ catch (e) {
432
+ console.warn('[snf evidence-blob]', rid, e.message);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ catch (e) {
438
+ console.warn('[snf evidence-blob] route err:', e.message);
439
+ }
440
+ res.json({ success: true, evidence_id: out.id, hash: out.hash, sig: out.sig, dedup: out.dedup, size: out.size });
441
+ }
442
+ catch (e) {
443
+ const msg = e.message;
444
+ const status = msg === 'not_dispute_party' ? 403
445
+ : msg === 'dispute_not_found' || msg === 'evidence_not_found' ? 404
446
+ : msg === 'evidence_too_large' ? 413
447
+ : msg === 'evidence_mime_not_allowed' ? 415
448
+ : 400;
449
+ res.status(status).json({ error: msg });
450
+ }
451
+ });
452
+ // 仲裁员:请求某方补证
453
+ app.post('/api/disputes/:id/request-evidence', (req, res) => {
454
+ const user = auth(req, res);
455
+ if (!user)
456
+ return;
457
+ const elig = isEligibleArbitrator(user.id);
458
+ if (!elig.ok)
459
+ return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
460
+ const { requested_from_id, evidence_types, description, deadline_hours = 48 } = req.body;
461
+ if (!requested_from_id || !description)
462
+ return void res.json({ error: '请指定被要求方和证据要求说明' });
463
+ if (!Array.isArray(evidence_types) || evidence_types.length === 0) {
464
+ return void res.json({ error: '请至少选择一种证据类型' });
465
+ }
466
+ const validTypes = ['text', 'image', 'video', 'document', 'chain_data'];
467
+ if (!evidence_types.every((t) => validTypes.includes(t))) {
468
+ return void res.json({ error: `证据类型无效,支持:${validTypes.join('/')}` });
469
+ }
470
+ const result = requestEvidence(db, req.params.id, user.id, requested_from_id, evidence_types, description, Number(deadline_hours));
471
+ if (!result.success)
472
+ return void res.json({ error: result.error });
473
+ res.json({ success: true, request_id: result.requestId });
474
+ });
475
+ }
@@ -0,0 +1,86 @@
1
+ import { readEvidenceBlob, withdrawEvidence, verifyEvidenceSig, listEvidence as listEvidenceFiles } from '../../layer3-trust/L3-1-dispute-engine/evidence-storage.js';
2
+ import { submitEvidenceForRequest } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
3
+ export function registerEvidenceRoutes(app, deps) {
4
+ const { db, auth, detectFraud } = deps;
5
+ // 下载证据 blob(仅参与方/仲裁员)
6
+ app.get('/api/evidence/:id/blob', (req, res) => {
7
+ const user = auth(req, res);
8
+ if (!user)
9
+ return;
10
+ try {
11
+ const out = readEvidenceBlob(db, req.params.id, user.id);
12
+ res.setHeader('Content-Type', out.mime);
13
+ res.setHeader('X-Content-Hash', out.hash);
14
+ res.setHeader('Cache-Control', 'private, max-age=300');
15
+ if (out.filename) {
16
+ res.setHeader('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(out.filename)}`);
17
+ }
18
+ res.send(out.blob);
19
+ }
20
+ catch (e) {
21
+ const msg = e.message;
22
+ const status = msg === 'not_dispute_party' ? 403
23
+ : msg === 'evidence_not_found' || msg === 'evidence_blob_missing' ? 404
24
+ : msg === 'evidence_withdrawn' ? 410
25
+ : msg === 'evidence_corrupted' ? 500
26
+ : 400;
27
+ res.status(status).json({ error: msg });
28
+ }
29
+ });
30
+ // 撤回证据(仅上传者,争议未结案时)
31
+ app.delete('/api/evidence/:id', (req, res) => {
32
+ const user = auth(req, res);
33
+ if (!user)
34
+ return;
35
+ try {
36
+ withdrawEvidence(db, req.params.id, user.id);
37
+ res.json({ success: true });
38
+ }
39
+ catch (e) {
40
+ const msg = e.message;
41
+ const status = msg === 'not_uploader' || msg === 'dispute_closed_cannot_withdraw' ? 403
42
+ : msg === 'evidence_not_found' ? 404
43
+ : 400;
44
+ res.status(status).json({ error: msg });
45
+ }
46
+ });
47
+ // 验签 — 任意参与方
48
+ app.get('/api/evidence/:id/verify', (req, res) => {
49
+ const user = auth(req, res);
50
+ if (!user)
51
+ return;
52
+ const ev = db.prepare('SELECT dispute_id FROM evidence WHERE id = ?').get(req.params.id);
53
+ if (!ev)
54
+ return void res.status(404).json({ error: 'evidence_not_found' });
55
+ try {
56
+ listEvidenceFiles(db, ev.dispute_id, user.id);
57
+ } // 复用鉴权
58
+ catch {
59
+ return void res.status(403).json({ error: 'not_dispute_party' });
60
+ }
61
+ res.json(verifyEvidenceSig(db, req.params.id));
62
+ });
63
+ // 当事人提交补充证据响应(仲裁员 request 后用)
64
+ app.post('/api/evidence-requests/:requestId/submit', (req, res) => {
65
+ const user = auth(req, res);
66
+ if (!user)
67
+ return;
68
+ const { evidence_type = 'text', description, file_hash } = req.body;
69
+ if (!description?.trim())
70
+ return void res.json({ error: '请填写证据内容' });
71
+ const rawDesc = String(description).trim();
72
+ const result = submitEvidenceForRequest(db, req.params.requestId, user.id, evidence_type, rawDesc, file_hash);
73
+ if (!result.success)
74
+ return void res.json({ error: result.error });
75
+ // 跨窗反诈:写 flag_reasons
76
+ const evReasons = detectFraud(rawDesc);
77
+ if (evReasons.length > 0 && result.evidenceId) {
78
+ try {
79
+ db.prepare(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`)
80
+ .run(JSON.stringify(evReasons), result.evidenceId);
81
+ }
82
+ catch { }
83
+ }
84
+ res.json({ success: true, evidence_id: result.evidenceId, anchor_hash: result.anchorHash, flag_reasons: evReasons });
85
+ });
86
+ }
@@ -0,0 +1,107 @@
1
+ import { createAnchor, verifyAnchorSignature, revokeAnchor, issueOwnershipToken, submitVerification, getAnchor, listAnchorsByProduct, listAnchorsBySeller, distributeAnchorRewards, ANCHOR_VERIFICATION_FEE_RECOMMENDED, } from '../../layer1-agent/L1-2-external-anchor/anchor-engine.js';
2
+ export function registerExternalAnchorsRoutes(app, deps) {
3
+ const { db, auth } = deps;
4
+ app.post('/api/external-anchors', (req, res) => {
5
+ const user = auth(req, res);
6
+ if (!user)
7
+ return;
8
+ const { product_id, platform, external_url, canonical, seller_node_url, verification_fee } = req.body || {};
9
+ if (!platform || !external_url || !canonical)
10
+ return void res.status(400).json({ error: 'platform / external_url / canonical 必填' });
11
+ try {
12
+ const r = createAnchor(db, {
13
+ sellerId: user.id,
14
+ productId: product_id ? String(product_id) : null,
15
+ platform: String(platform), externalUrl: String(external_url),
16
+ canonical: canonical,
17
+ sellerNodeUrl: seller_node_url ? String(seller_node_url) : null,
18
+ verificationFee: verification_fee != null ? Number(verification_fee) : undefined,
19
+ });
20
+ res.json({ ok: true, ...r });
21
+ }
22
+ catch (e) {
23
+ res.status(400).json({ error: e.message });
24
+ }
25
+ });
26
+ // 透出推荐 fee + anchor 的奖励情况
27
+ app.get('/api/external-anchors/:id/rewards', (req, res) => {
28
+ const a = getAnchor(db, req.params.id);
29
+ if (!a)
30
+ return void res.status(404).json({ error: 'anchor 不存在' });
31
+ const verifications = db.prepare(`
32
+ SELECT verifier_id, verifier_role, content_matches, token_found, reward_amount, verified_at
33
+ FROM external_anchor_verifications WHERE anchor_id = ? ORDER BY verified_at ASC
34
+ `).all(req.params.id);
35
+ res.json({
36
+ verification_fee: a.verification_fee || 0,
37
+ fee_paid_out: !!a.fee_paid_out,
38
+ ownership_verified: a.ownership_verified,
39
+ recommended_fee: ANCHOR_VERIFICATION_FEE_RECOMMENDED,
40
+ verifications,
41
+ total_paid_out: verifications.reduce((s, v) => s + Number(v.reward_amount || 0), 0),
42
+ });
43
+ });
44
+ // 手动 distribute(admin/arbitrator 补救:anchor 已 community 但 fee_paid_out=0)
45
+ app.post('/api/external-anchors/:id/distribute-rewards', (req, res) => {
46
+ const user = auth(req, res);
47
+ if (!user)
48
+ return;
49
+ if (user.role !== 'admin' && user.role !== 'arbitrator')
50
+ return void res.status(403).json({ error: '仅管理员/仲裁员可手动分发' });
51
+ const paid = distributeAnchorRewards(db, req.params.id);
52
+ res.json({ ok: true, paid });
53
+ });
54
+ app.get('/api/external-anchors/by-product/:id', (req, res) => {
55
+ res.json({ items: listAnchorsByProduct(db, req.params.id) });
56
+ });
57
+ app.get('/api/external-anchors/by-seller/:id', (req, res) => {
58
+ res.json({ items: listAnchorsBySeller(db, req.params.id) });
59
+ });
60
+ app.get('/api/external-anchors/:id', (req, res) => {
61
+ const a = getAnchor(db, req.params.id);
62
+ if (!a)
63
+ return void res.status(404).json({ error: 'anchor 不存在' });
64
+ res.json(a);
65
+ });
66
+ app.get('/api/external-anchors/:id/verify-sig', (req, res) => {
67
+ res.json(verifyAnchorSignature(db, req.params.id));
68
+ });
69
+ app.post('/api/external-anchors/:id/revoke', (req, res) => {
70
+ const user = auth(req, res);
71
+ if (!user)
72
+ return;
73
+ const r = revokeAnchor(db, req.params.id, user.id, String(req.body?.reason || 'manual'));
74
+ if (!r.ok)
75
+ return void res.status(400).json({ error: r.reason });
76
+ res.json({ ok: true });
77
+ });
78
+ app.post('/api/external-anchors/:id/issue-token', (req, res) => {
79
+ const user = auth(req, res);
80
+ if (!user)
81
+ return;
82
+ const r = issueOwnershipToken(db, req.params.id, user.id);
83
+ if (!r.ok)
84
+ return void res.status(400).json({ error: r.reason });
85
+ res.json(r);
86
+ });
87
+ // verifier 提交独立验证(任何已登录用户可做)
88
+ app.post('/api/external-anchors/:id/verify', (req, res) => {
89
+ const user = auth(req, res);
90
+ if (!user)
91
+ return;
92
+ const { submitted_canonical, token_found, notes } = req.body || {};
93
+ if (!submitted_canonical)
94
+ return void res.status(400).json({ error: '请提交独立提取的 canonical 数据' });
95
+ const r = submitVerification(db, {
96
+ anchorId: req.params.id,
97
+ verifierId: user.id,
98
+ verifierRole: user.role,
99
+ submittedCanonical: submitted_canonical,
100
+ tokenFoundInExternal: !!token_found,
101
+ notes: notes ? String(notes) : undefined,
102
+ });
103
+ if (!r.ok)
104
+ return void res.status(400).json({ error: r.reason });
105
+ res.json(r);
106
+ });
107
+ }