@seasonkoh/webaz 0.1.8 → 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 +3543 -852
  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 +31230 -2345
  22. package/dist/pwa/public/i18n.js +5282 -111
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +4 -1
  25. package/dist/pwa/public/manifest.json +39 -4
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +278 -5
  28. package/dist/pwa/public/sw.js +41 -2
  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 +9247 -2097
  153. package/package.json +8 -3
@@ -0,0 +1,615 @@
1
+ // ─── 域常量 ───────────────────────────────────────────────
2
+ export const CLAIM_STAKE_DEFAULT = 10; // 买家发起质押 10 WAZ
3
+ export const CLAIM_DEADLINE_HOURS = 48; // 接单 + 投票截止
4
+ export const CLAIM_SELLER_EXTENSION_HOURS = 24; // 卖家提交证据后延期
5
+ export const CLAIM_VERIFIERS_NEEDED = 3; // 共识阈值
6
+ export const CLAIM_VERIFIER_MIN_REP = 200; // reputation_scores.total_points 门槛
7
+ export const CLAIM_VERIFIER_MAX_ACTIVE = 5; // 同时进行中任务上限
8
+ const CLAIM_VALID_TARGETS = new Set(['price', 'commission', 'protection', 'return', 'warranty', 'handling', 'other']);
9
+ export const CLAIM_TARGET_LABEL_ZH = {
10
+ price: '价格优势', commission: '分享佣金', protection: '协议保障',
11
+ return: '退货条款', warranty: '质保条款', handling: '发货时效', other: '其他理由',
12
+ };
13
+ const CLAIM_VALID_VOTES = new Set(['pass', 'fail', 'no_fault', 'abstain']);
14
+ // V3:abstain 不计入 3-vote 共识、不参与 majority、不触发 outlier
15
+ const CLAIM_SELLER_FINE_RATE = 0.10; // pass 时扣 product.stake_amount × 10%
16
+ const CLAIM_NO_FAULT_SUBSIDY = 1; // no_fault 路径协议池补贴每个 verifier 1 WAZ
17
+ // 跨域共用(server.ts checkVerifierOutlier 跨 6 套 vote table 聚合也用同一阈值)
18
+ export const CLAIM_SUSPEND_THRESHOLD = 3; // 180d 内 ≥3 次 outlier → 30d 冻结
19
+ export const CLAIM_REVOKE_THRESHOLD = 5; // 180d 内 ≥5 次 outlier → 永封
20
+ export const CLAIM_SUSPEND_DAYS = 30;
21
+ export const CLAIM_OUTLIER_WINDOW_DAYS = 180;
22
+ // ─── helpers (module-level, db 通过参数传) ───────────────────
23
+ // 2026-05-22 V2:通知所有资格内 verifier 有新 claim 任务
24
+ export function notifyEligibleVerifiers(db, generateId, args) {
25
+ const { taskId: _taskId, productTitle, claimTargetLabel, buyerId, sellerId, notificationType } = args;
26
+ // ① whitelist 用户(含内部审核员) ② reputation_scores >= CLAIM_VERIFIER_MIN_REP
27
+ const rows = db.prepare(`
28
+ SELECT DISTINCT u.id
29
+ FROM users u
30
+ WHERE u.id IN (
31
+ SELECT user_id FROM verifier_whitelist
32
+ UNION
33
+ SELECT user_id FROM reputation_scores WHERE total_points >= ?
34
+ )
35
+ AND u.id NOT IN (?, ?)
36
+ AND COALESCE(u.notify_claim_tasks, 1) = 1
37
+ AND NOT EXISTS (
38
+ SELECT 1 FROM claim_verifier_suspensions s
39
+ WHERE s.user_id = u.id
40
+ AND (s.type = 'revoked' OR (s.until_at IS NOT NULL AND s.until_at > datetime('now')))
41
+ )
42
+ `).all(CLAIM_VERIFIER_MIN_REP, buyerId, sellerId);
43
+ if (rows.length === 0)
44
+ return 0;
45
+ const title = notificationType === 'claim_new'
46
+ ? `🔎 新验证任务:${claimTargetLabel}`
47
+ : `📎 验证任务有新证据:${claimTargetLabel}`;
48
+ const body = notificationType === 'claim_new'
49
+ ? `「${productTitle}」 — 去 #claims 广场查看并投票`
50
+ : `「${productTitle}」卖家提交了新证据,截止延期 24h`;
51
+ const ins = db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`);
52
+ db.transaction(() => {
53
+ for (const r of rows) {
54
+ try {
55
+ ins.run(generateId('ntf'), r.id, notificationType, title, body, null);
56
+ }
57
+ catch { }
58
+ }
59
+ })();
60
+ return rows.length;
61
+ }
62
+ export function isEligibleClaimVerifier(db, userId) {
63
+ // ── M7.3b:先看是否被禁言 / 永封 ──
64
+ const sus = db.prepare(`SELECT type, until_at FROM claim_verifier_suspensions
65
+ WHERE user_id = ? AND (type = 'revoked' OR until_at > datetime('now'))
66
+ ORDER BY created_at DESC LIMIT 1`).get(userId);
67
+ if (sus) {
68
+ if (sus.type === 'revoked')
69
+ return { ok: false, reason: '该账号已永久撤销 verifier 资格(累计 5 次 outlier)' };
70
+ return { ok: false, reason: `账号 verifier 资格被冻结至 ${sus.until_at}(累计 3 次 outlier)` };
71
+ }
72
+ // ① verifier_whitelist 一票通过
73
+ const wl = db.prepare('SELECT user_id FROM verifier_whitelist WHERE user_id = ?').get(userId);
74
+ if (wl)
75
+ return { ok: true, via: 'whitelist' };
76
+ // ② 信誉门槛
77
+ const rep = db.prepare('SELECT total_points FROM reputation_scores WHERE user_id = ?').get(userId);
78
+ if (rep && (rep.total_points ?? 0) >= CLAIM_VERIFIER_MIN_REP)
79
+ return { ok: true, via: 'reputation' };
80
+ return { ok: false, reason: `需要 verifier_whitelist 或 信誉积分 ≥ ${CLAIM_VERIFIER_MIN_REP}(当前 ${rep?.total_points ?? 0})` };
81
+ }
82
+ export function activeClaimTaskCountForVerifier(db, userId) {
83
+ const r = db.prepare(`
84
+ SELECT COUNT(DISTINCT cvv.task_id) as n
85
+ FROM claim_verification_votes cvv
86
+ JOIN claim_verification_tasks cvt ON cvt.id = cvv.task_id
87
+ WHERE cvv.verifier_id = ? AND cvt.status = 'open'
88
+ `).get(userId);
89
+ return r.n;
90
+ }
91
+ // M7.3b:单个 outlier 处罚检查
92
+ function checkAndApplyOutlierStrike(db, generateId, userId) {
93
+ const cnt = db.prepare(`
94
+ SELECT COUNT(*) as n FROM claim_verification_votes cvv
95
+ JOIN claim_verification_tasks cvt ON cvt.id = cvv.task_id
96
+ WHERE cvv.verifier_id = ?
97
+ AND cvv.was_majority = 0
98
+ AND cvt.resolved_at IS NOT NULL
99
+ AND cvt.resolved_at >= datetime('now', '-${CLAIM_OUTLIER_WINDOW_DAYS} days')
100
+ `).get(userId).n;
101
+ const existing = db.prepare(`SELECT type, outlier_count FROM claim_verifier_suspensions
102
+ WHERE user_id = ? AND (type = 'revoked' OR until_at > datetime('now'))
103
+ ORDER BY created_at DESC LIMIT 1`).get(userId);
104
+ if (existing?.type === 'revoked')
105
+ return { strikes_180d: cnt };
106
+ if (cnt >= CLAIM_REVOKE_THRESHOLD && (!existing || existing.outlier_count < CLAIM_REVOKE_THRESHOLD)) {
107
+ db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, reason, outlier_count)
108
+ VALUES (?,?, 'revoked', ?, ?)`).run(generateId('cvs'), userId, `180d 内累计 ${cnt} 次 outlier`, cnt);
109
+ return { strikes_180d: cnt, suspension: { type: 'revoked', until_at: null } };
110
+ }
111
+ if (cnt >= CLAIM_SUSPEND_THRESHOLD && !existing) {
112
+ const until = new Date(Date.now() + CLAIM_SUSPEND_DAYS * 86400_000).toISOString();
113
+ db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, until_at, reason, outlier_count)
114
+ VALUES (?,?, 'suspended', ?, ?, ?)`).run(generateId('cvs'), userId, until, `180d 内累计 ${cnt} 次 outlier`, cnt);
115
+ return { strikes_180d: cnt, suspension: { type: 'suspended', until_at: until } };
116
+ }
117
+ return { strikes_180d: cnt };
118
+ }
119
+ // 给一组 user_id 平均分发金额
120
+ function distributePool(db, userIds, total) {
121
+ if (userIds.length === 0 || total <= 0)
122
+ return;
123
+ const each = Math.floor((total / userIds.length) * 100) / 100;
124
+ let used = 0;
125
+ for (let i = 0; i < userIds.length; i++) {
126
+ const amt = i === userIds.length - 1 ? Math.round((total - used) * 100) / 100 : each;
127
+ used += amt;
128
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(amt, userIds[i]);
129
+ }
130
+ }
131
+ // M7.3b 核心:三路径结算
132
+ export function settleClaimTask(db, generateId, taskId) {
133
+ const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?').get(taskId);
134
+ if (!task)
135
+ return { ok: false, reason: 'task not found' };
136
+ if (String(task.status).startsWith('resolved_') || String(task.status).startsWith('timeout_')) {
137
+ return { ok: false, reason: '已结算' };
138
+ }
139
+ const allVotes = db.prepare('SELECT id, verifier_id, vote FROM claim_verification_votes WHERE task_id = ?').all(taskId);
140
+ const votes = allVotes.filter(v => v.vote !== 'abstain');
141
+ const counts = { pass: 0, fail: 0, no_fault: 0 };
142
+ for (const v of votes)
143
+ counts[v.vote] = (counts[v.vote] || 0) + 1;
144
+ let majority = 'no_fault';
145
+ let path = 'no_fault';
146
+ if (votes.length === 0) {
147
+ path = 'timeout_no_fault';
148
+ majority = 'no_fault';
149
+ }
150
+ else {
151
+ const maxN = Math.max(counts.pass, counts.fail, counts.no_fault);
152
+ const winners = ['pass', 'fail', 'no_fault'].filter(k => counts[k] === maxN);
153
+ if (winners.length > 1)
154
+ majority = 'no_fault';
155
+ else
156
+ majority = winners[0];
157
+ path = majority === 'pass' ? 'pass' : majority === 'fail' ? 'fail' : 'no_fault';
158
+ }
159
+ const buyerId = task.buyer_id;
160
+ const sellerId = task.seller_id;
161
+ const productId = task.product_id;
162
+ const stake = Number(task.stake_buyer);
163
+ const payouts = { path, majority, stake_buyer: stake };
164
+ db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(stake, buyerId);
165
+ const majorityVoters = majority === 'no_fault'
166
+ ? votes.map(v => v.verifier_id)
167
+ : votes.filter(v => v.vote === majority).map(v => v.verifier_id);
168
+ if (path === 'pass') {
169
+ const refund = Math.round(stake * 0.5 * 100) / 100;
170
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refund, buyerId);
171
+ const voterPool = Math.round(stake * 0.5 * 100) / 100;
172
+ payouts.buyer_refund = refund;
173
+ payouts.voter_reward_from_buyer = voterPool;
174
+ distributePool(db, majorityVoters, voterPool);
175
+ const prod = db.prepare('SELECT stake_amount, stake_locked_at FROM products WHERE id = ?').get(productId);
176
+ if (prod && prod.stake_amount > 0) {
177
+ const fine = Math.round(prod.stake_amount * CLAIM_SELLER_FINE_RATE * 100) / 100;
178
+ const sellerWallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?').get(sellerId);
179
+ if (prod.stake_locked_at) {
180
+ const fromStaked = Math.min(fine, sellerWallet.staked || 0);
181
+ db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(fromStaked, sellerId);
182
+ const remain = fine - fromStaked;
183
+ if (remain > 0) {
184
+ db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(remain, sellerId);
185
+ }
186
+ }
187
+ else {
188
+ const fromBalance = Math.min(fine, sellerWallet.balance || 0);
189
+ db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(fromBalance, sellerId);
190
+ db.prepare("UPDATE products SET stake_locked_at = datetime('now') WHERE id = ?").run(productId);
191
+ }
192
+ const halfFine = Math.round(fine * 0.5 * 100) / 100;
193
+ distributePool(db, majorityVoters, halfFine);
194
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(fine - halfFine);
195
+ payouts.seller_fine = fine;
196
+ payouts.voter_reward_from_seller_fine = halfFine;
197
+ payouts.protocol_share = fine - halfFine;
198
+ }
199
+ db.prepare(`UPDATE claim_verification_tasks SET status = 'resolved_pass', majority_vote = ?, resolved_at = datetime('now') WHERE id = ?`).run(majority, taskId);
200
+ }
201
+ else if (path === 'fail') {
202
+ const voterPool = Math.round(stake * 0.5 * 100) / 100;
203
+ distributePool(db, majorityVoters, voterPool);
204
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(stake - voterPool);
205
+ payouts.buyer_refund = 0;
206
+ payouts.voter_reward = voterPool;
207
+ payouts.protocol_share = stake - voterPool;
208
+ db.prepare(`UPDATE claim_verification_tasks SET status = 'resolved_fail', majority_vote = ?, resolved_at = datetime('now') WHERE id = ?`).run(majority, taskId);
209
+ }
210
+ else {
211
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(stake, buyerId);
212
+ const allVoters = votes.map(v => v.verifier_id);
213
+ const idealSubsidy = CLAIM_NO_FAULT_SUBSIDY * allVoters.length;
214
+ const sp = db.prepare("SELECT balance FROM wallets WHERE user_id = 'sys_protocol'").get();
215
+ const available = Math.max(0, sp?.balance ?? 0);
216
+ const subsidy = Math.min(idealSubsidy, available);
217
+ if (subsidy > 0) {
218
+ db.prepare("UPDATE wallets SET balance = balance - ? WHERE user_id = 'sys_protocol'").run(subsidy);
219
+ distributePool(db, allVoters, subsidy);
220
+ }
221
+ payouts.buyer_refund = stake;
222
+ payouts.voter_subsidy = subsidy;
223
+ if (subsidy < idealSubsidy)
224
+ payouts.voter_subsidy_shortfall = idealSubsidy - subsidy;
225
+ const finalStatus = votes.length === 0 ? 'timeout_no_fault' : 'resolved_no_fault';
226
+ db.prepare(`UPDATE claim_verification_tasks SET status = ?, majority_vote = ?, resolved_at = datetime('now') WHERE id = ?`).run(finalStatus, majority, taskId);
227
+ }
228
+ // 标记每张票是否属于 majority 派
229
+ for (const v of allVotes) {
230
+ if (v.vote === 'abstain') {
231
+ db.prepare('UPDATE claim_verification_votes SET was_majority = NULL WHERE id = ?').run(v.id);
232
+ }
233
+ else {
234
+ const wasMaj = v.vote === majority ? 1 : 0;
235
+ db.prepare('UPDATE claim_verification_votes SET was_majority = ? WHERE id = ?').run(wasMaj, v.id);
236
+ }
237
+ }
238
+ // outlier 处罚
239
+ const strikes = {};
240
+ if (votes.length >= 2) {
241
+ for (const v of votes) {
242
+ if (v.vote !== majority) {
243
+ const r = checkAndApplyOutlierStrike(db, generateId, v.verifier_id);
244
+ strikes[v.verifier_id] = r;
245
+ }
246
+ }
247
+ }
248
+ payouts.outlier_strikes = strikes;
249
+ db.prepare('UPDATE orders SET has_pending_claim = 0 WHERE id = ?').run(task.order_id);
250
+ return { ok: true, path, majority, payouts };
251
+ }
252
+ // 扫描需要结算的任务(5min enforcement cron 调用)
253
+ export function processClaimTaskQueue(db, generateId) {
254
+ const details = [];
255
+ let sealed = 0, timeout = 0;
256
+ try {
257
+ const sealedTasks = db.prepare(`SELECT id FROM claim_verification_tasks WHERE status = 'sealed'`).all();
258
+ for (const t of sealedTasks) {
259
+ const r = settleClaimTask(db, generateId, t.id);
260
+ if (r.ok) {
261
+ sealed++;
262
+ details.push({ task_id: t.id, ...r });
263
+ }
264
+ }
265
+ const timedOut = db.prepare(`SELECT id FROM claim_verification_tasks WHERE status = 'open' AND deadline_at < datetime('now')`).all();
266
+ for (const t of timedOut) {
267
+ const r = settleClaimTask(db, generateId, t.id);
268
+ if (r.ok) {
269
+ timeout++;
270
+ details.push({ task_id: t.id, ...r });
271
+ }
272
+ }
273
+ }
274
+ catch (e) {
275
+ console.error('[M7.3b processClaimTaskQueue]', e.message);
276
+ }
277
+ return { sealed, timeout, details };
278
+ }
279
+ export function registerClaimVerifyRoutes(app, deps) {
280
+ const { db, auth, generateId, requireHumanPresence } = deps;
281
+ // 买家发起 claim 验证任务(绑定 paid 及之后的订单)
282
+ app.post('/api/orders/:id/claim-verification', (req, res) => {
283
+ const user = auth(req, res);
284
+ if (!user)
285
+ return;
286
+ const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id);
287
+ if (!order)
288
+ return void res.status(404).json({ error: '订单不存在' });
289
+ if (order.buyer_id !== user.id)
290
+ return void res.status(403).json({ error: '仅订单买家可发起验证' });
291
+ const blockedStatuses = new Set(['created', 'cancelled', 'completed', 'refunded']);
292
+ if (blockedStatuses.has(order.status)) {
293
+ return void res.status(400).json({ error: `当前订单状态(${order.status})不可发起验证` });
294
+ }
295
+ const existing = db.prepare('SELECT id FROM claim_verification_tasks WHERE order_id = ?').get(req.params.id);
296
+ if (existing)
297
+ return void res.status(409).json({ error: '该订单已存在验证任务(不可撤销)', task_id: existing.id });
298
+ const claim_target = String(req.body?.claim_target || '').trim();
299
+ if (!CLAIM_VALID_TARGETS.has(claim_target)) {
300
+ return void res.status(400).json({ error: `claim_target 必须是 ${[...CLAIM_VALID_TARGETS].join(' / ')} 之一` });
301
+ }
302
+ const claim_text = String(req.body?.claim_text || '').trim();
303
+ if (claim_text.length < 6 || claim_text.length > 500) {
304
+ return void res.status(400).json({ error: 'claim_text 长度需 6-500 字' });
305
+ }
306
+ const evidence_uri = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
307
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
308
+ const stake = CLAIM_STAKE_DEFAULT;
309
+ if (!wallet || wallet.balance < stake) {
310
+ return void res.status(400).json({ error: `余额不足:发起需锁 ${stake} WAZ,当前余额 ${wallet?.balance ?? 0} WAZ` });
311
+ }
312
+ const id = generateId('cvt');
313
+ const deadline = new Date(Date.now() + CLAIM_DEADLINE_HOURS * 3600_000).toISOString();
314
+ const sellerId = order.seller_id;
315
+ db.prepare(`INSERT INTO claim_verification_tasks
316
+ (id, order_id, buyer_id, seller_id, product_id, claim_target, claim_text, evidence_uri, stake_buyer, deadline_at, status)
317
+ VALUES (?,?,?,?,?,?,?,?,?,?, 'open')`).run(id, req.params.id, user.id, sellerId, order.product_id, claim_target, claim_text, evidence_uri, stake, deadline);
318
+ db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
319
+ .run(stake, stake, user.id);
320
+ db.prepare('UPDATE orders SET has_pending_claim = 1 WHERE id = ?').run(req.params.id);
321
+ const productTitle = db.prepare('SELECT title FROM products WHERE id = ?').get(order.product_id)?.title || '—';
322
+ const claimLabel = CLAIM_TARGET_LABEL_ZH[claim_target] || claim_target;
323
+ try {
324
+ db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`)
325
+ .run(generateId('ntf'), sellerId, 'claim_new', `⚠️ 买家发起验证:${claimLabel}`, `订单「${productTitle}」 — 48h 内提交证据可延期至 verifier 共识结案`, req.params.id);
326
+ }
327
+ catch (e) {
328
+ console.error('[V2 notify seller]', e.message);
329
+ }
330
+ try {
331
+ const notified = notifyEligibleVerifiers(db, generateId, {
332
+ taskId: id, productTitle, claimTargetLabel: claimLabel,
333
+ buyerId: user.id, sellerId,
334
+ notificationType: 'claim_new',
335
+ });
336
+ console.log(`[V2] claim_new ${id} notified ${notified} verifiers`);
337
+ }
338
+ catch (e) {
339
+ console.error('[V2 notify verifiers]', e.message);
340
+ }
341
+ res.json({ success: true, task_id: id, deadline_at: deadline, stake_locked: stake });
342
+ });
343
+ // 通过 order_id 查关联 task
344
+ app.get('/api/orders/:id/claim-task', (req, res) => {
345
+ const user = auth(req, res);
346
+ if (!user)
347
+ return;
348
+ const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE order_id = ?')
349
+ .get(req.params.id);
350
+ if (!task)
351
+ return void res.json({ task: null });
352
+ const hasVoted = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
353
+ .get(task.id, user.id);
354
+ const isParty = task.buyer_id === user.id || task.seller_id === user.id;
355
+ const elig = isEligibleClaimVerifier(db, user.id);
356
+ if (!isParty && !hasVoted && !elig.ok)
357
+ return void res.json({ task: null, visibility: 'restricted' });
358
+ const votes = db.prepare(`SELECT verifier_id, vote, voted_at FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`).all(task.id);
359
+ res.json({ task, votes, votes_needed: CLAIM_VERIFIERS_NEEDED });
360
+ });
361
+ // 列出可接的 open 任务
362
+ app.get('/api/claim-tasks/available', (req, res) => {
363
+ const user = auth(req, res);
364
+ if (!user)
365
+ return;
366
+ const elig = isEligibleClaimVerifier(db, user.id);
367
+ if (!elig.ok)
368
+ return void res.status(403).json({ error: elig.reason, eligible: false });
369
+ const active = activeClaimTaskCountForVerifier(db, user.id);
370
+ if (active >= CLAIM_VERIFIER_MAX_ACTIVE) {
371
+ return void res.status(429).json({ error: `已有 ${active} 个进行中任务(上限 ${CLAIM_VERIFIER_MAX_ACTIVE}),请先完成`, active });
372
+ }
373
+ const rows = db.prepare(`
374
+ SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target, cvt.claim_text,
375
+ cvt.evidence_uri, cvt.seller_evidence_uri, cvt.deadline_at, cvt.created_at,
376
+ (SELECT COUNT(*) FROM claim_verification_votes WHERE task_id = cvt.id AND vote != 'abstain') as votes_count,
377
+ p.title as product_title
378
+ FROM claim_verification_tasks cvt
379
+ LEFT JOIN products p ON p.id = cvt.product_id
380
+ WHERE cvt.status = 'open'
381
+ AND cvt.buyer_id != ? AND cvt.seller_id != ?
382
+ AND NOT EXISTS (SELECT 1 FROM claim_verification_votes WHERE task_id = cvt.id AND verifier_id = ?)
383
+ AND (SELECT COUNT(*) FROM claim_verification_votes WHERE task_id = cvt.id AND vote != 'abstain') < ?
384
+ ORDER BY cvt.created_at ASC
385
+ LIMIT 50
386
+ `).all(user.id, user.id, user.id, CLAIM_VERIFIERS_NEEDED);
387
+ res.json({ eligible: true, via: elig.via, active, max_active: CLAIM_VERIFIER_MAX_ACTIVE, tasks: rows });
388
+ });
389
+ // verifier 投票 — 铁律 §4
390
+ app.post('/api/claim-tasks/:id/vote', (req, res) => {
391
+ const user = auth(req, res);
392
+ if (!user)
393
+ return;
394
+ const elig = isEligibleClaimVerifier(db, user.id);
395
+ if (!elig.ok)
396
+ return void res.status(403).json({ error: elig.reason });
397
+ // 2026-05-23 Agent 治理铁律:投票需真实人工
398
+ const hpCheck = requireHumanPresence(user.id, 'vote', req.body?.webauthn_token, 'require_human_presence_for_vote', (data) => {
399
+ const d = data;
400
+ return d == null || d.task_id === req.params.id;
401
+ });
402
+ if (!hpCheck.ok)
403
+ return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
404
+ const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?').get(req.params.id);
405
+ if (!task)
406
+ return void res.status(404).json({ error: '任务不存在' });
407
+ if (task.status !== 'open')
408
+ return void res.status(400).json({ error: `任务状态为 ${task.status},不接受投票` });
409
+ if (task.buyer_id === user.id)
410
+ return void res.status(403).json({ error: '买家不可对自己的发起任务投票' });
411
+ if (task.seller_id === user.id)
412
+ return void res.status(403).json({ error: '卖家不可对自己的商品投票' });
413
+ const vote = String(req.body?.vote || '').trim();
414
+ if (!CLAIM_VALID_VOTES.has(vote)) {
415
+ return void res.status(400).json({ error: `vote 必须是 ${[...CLAIM_VALID_VOTES].join(' / ')}` });
416
+ }
417
+ const evidence_uri = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
418
+ const note = req.body?.note ? String(req.body.note).trim().slice(0, 500) : null;
419
+ const dup = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
420
+ .get(req.params.id, user.id);
421
+ if (dup)
422
+ return void res.status(409).json({ error: '已投过票' });
423
+ const votesNow = db.prepare(`SELECT COUNT(*) as n FROM claim_verification_votes WHERE task_id = ? AND vote != 'abstain'`)
424
+ .get(req.params.id).n;
425
+ if (votesNow >= CLAIM_VERIFIERS_NEEDED)
426
+ return void res.status(409).json({ error: '已收齐共识票数,等待结算' });
427
+ const active = activeClaimTaskCountForVerifier(db, user.id);
428
+ if (active >= CLAIM_VERIFIER_MAX_ACTIVE) {
429
+ return void res.status(429).json({ error: `已有 ${active} 个进行中任务(上限 ${CLAIM_VERIFIER_MAX_ACTIVE})` });
430
+ }
431
+ const id = generateId('cvv');
432
+ try {
433
+ db.prepare(`INSERT INTO claim_verification_votes (id, task_id, verifier_id, vote, evidence_uri, note) VALUES (?,?,?,?,?,?)`)
434
+ .run(id, req.params.id, user.id, vote, evidence_uri, note);
435
+ }
436
+ catch {
437
+ return void res.status(409).json({ error: '投票失败(可能并发重复)' });
438
+ }
439
+ // 收齐 3 共识票 → 标记 sealed 并立即结算
440
+ const after = db.prepare(`SELECT COUNT(*) as n FROM claim_verification_votes WHERE task_id = ? AND vote != 'abstain'`)
441
+ .get(req.params.id).n;
442
+ let settlement = null;
443
+ if (after >= CLAIM_VERIFIERS_NEEDED) {
444
+ db.prepare(`UPDATE claim_verification_tasks SET status = 'sealed' WHERE id = ? AND status = 'open'`)
445
+ .run(req.params.id);
446
+ settlement = settleClaimTask(db, generateId, req.params.id);
447
+ }
448
+ res.json({
449
+ success: true, vote_id: id, votes_collected: after,
450
+ sealed: after >= CLAIM_VERIFIERS_NEEDED,
451
+ settlement: settlement?.ok ? { path: settlement.path, majority: settlement.majority, payouts: settlement.payouts } : undefined,
452
+ });
453
+ });
454
+ // 我相关的任务(必须在 /:id 之前注册,否则被 /:id 截获)
455
+ app.get('/api/claim-tasks/mine', (req, res) => {
456
+ const user = auth(req, res);
457
+ if (!user)
458
+ return;
459
+ const asBuyer = db.prepare(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
460
+ FROM claim_verification_tasks WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
461
+ const asSeller = db.prepare(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
462
+ FROM claim_verification_tasks WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
463
+ const asVerifier = db.prepare(`
464
+ SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target, cvt.status, cvt.deadline_at, cvt.created_at,
465
+ cvv.vote, cvv.voted_at
466
+ FROM claim_verification_votes cvv
467
+ JOIN claim_verification_tasks cvt ON cvt.id = cvv.task_id
468
+ WHERE cvv.verifier_id = ?
469
+ ORDER BY cvv.voted_at DESC
470
+ LIMIT 50`).all(user.id);
471
+ res.json({ as_buyer: asBuyer, as_seller: asSeller, as_verifier: asVerifier });
472
+ });
473
+ // 通知偏好
474
+ app.post('/api/me/notify-claim-tasks', (req, res) => {
475
+ const user = auth(req, res);
476
+ if (!user)
477
+ return;
478
+ const enabled = req.body?.enabled === false ? 0 : 1;
479
+ db.prepare('UPDATE users SET notify_claim_tasks = ? WHERE id = ?').run(enabled, user.id);
480
+ res.json({ success: true, notify_claim_tasks: enabled });
481
+ });
482
+ app.get('/api/me/notify-claim-tasks', (req, res) => {
483
+ const user = auth(req, res);
484
+ if (!user)
485
+ return;
486
+ const row = db.prepare('SELECT COALESCE(notify_claim_tasks, 1) as enabled FROM users WHERE id = ?').get(user.id);
487
+ res.json({ notify_claim_tasks: row?.enabled ?? 1 });
488
+ });
489
+ // 公开 #claims 广场(无 auth — 透明性是验证声明信任的前提)
490
+ app.get('/api/claims/public', (req, res) => {
491
+ const status = String(req.query.status || 'open');
492
+ const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 30));
493
+ let where;
494
+ let orderBy;
495
+ if (status === 'open') {
496
+ where = `cvt.status = 'open'`;
497
+ orderBy = `cvt.deadline_at ASC`;
498
+ }
499
+ else if (status === 'sealed') {
500
+ where = `cvt.status = 'sealed'`;
501
+ orderBy = `cvt.created_at DESC`;
502
+ }
503
+ else if (status === 'resolved') {
504
+ where = `cvt.status LIKE 'resolved_%' OR cvt.status LIKE 'timeout_%'`;
505
+ orderBy = `cvt.resolved_at DESC`;
506
+ }
507
+ else {
508
+ where = `1=1`;
509
+ orderBy = `cvt.created_at DESC`;
510
+ }
511
+ const rows = db.prepare(`
512
+ SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target,
513
+ SUBSTR(cvt.claim_text, 1, 140) as claim_excerpt,
514
+ cvt.evidence_uri IS NOT NULL as has_buyer_evidence,
515
+ cvt.seller_evidence_uri IS NOT NULL as has_seller_evidence,
516
+ cvt.deadline_at, cvt.created_at, cvt.resolved_at,
517
+ cvt.status, cvt.majority_vote,
518
+ (SELECT COUNT(*) FROM claim_verification_votes WHERE task_id = cvt.id AND vote != 'abstain') as votes_count,
519
+ p.title as product_title, p.images as product_images, p.price as product_price
520
+ FROM claim_verification_tasks cvt
521
+ LEFT JOIN products p ON p.id = cvt.product_id
522
+ WHERE ${where}
523
+ ORDER BY ${orderBy}
524
+ LIMIT ?
525
+ `).all(limit);
526
+ const items = rows.map(r => {
527
+ let firstImage = null;
528
+ try {
529
+ const arr = JSON.parse(r.product_images || '[]');
530
+ if (Array.isArray(arr) && arr.length > 0)
531
+ firstImage = String(arr[0]);
532
+ }
533
+ catch { }
534
+ return {
535
+ id: r.id,
536
+ product_id: r.product_id,
537
+ product_title: r.product_title,
538
+ product_image: firstImage,
539
+ product_price: r.product_price,
540
+ claim_target: r.claim_target,
541
+ claim_excerpt: r.claim_excerpt,
542
+ has_buyer_evidence: !!r.has_buyer_evidence,
543
+ has_seller_evidence: !!r.has_seller_evidence,
544
+ votes_count: r.votes_count,
545
+ votes_needed: CLAIM_VERIFIERS_NEEDED,
546
+ status: r.status,
547
+ majority_vote: r.majority_vote,
548
+ deadline_at: r.deadline_at,
549
+ created_at: r.created_at,
550
+ resolved_at: r.resolved_at,
551
+ };
552
+ });
553
+ res.json({ items, votes_needed: CLAIM_VERIFIERS_NEEDED });
554
+ });
555
+ // 任务详情
556
+ app.get('/api/claim-tasks/:id', (req, res) => {
557
+ const user = auth(req, res);
558
+ if (!user)
559
+ return;
560
+ const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?')
561
+ .get(req.params.id);
562
+ if (!task)
563
+ return void res.status(404).json({ error: '任务不存在' });
564
+ const hasVoted = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
565
+ .get(req.params.id, user.id);
566
+ const isParty = task.buyer_id === user.id || task.seller_id === user.id;
567
+ const elig = isEligibleClaimVerifier(db, user.id);
568
+ const canRead = isParty || !!hasVoted || elig.ok;
569
+ if (!canRead) {
570
+ return void res.status(403).json({ error: '仅当事人或已投票 / 资格内 verifier 可见任务详情' });
571
+ }
572
+ const votes = db.prepare(`SELECT id, verifier_id, vote, evidence_uri, note, voted_at
573
+ FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`).all(req.params.id);
574
+ res.json({ task, votes, votes_needed: CLAIM_VERIFIERS_NEEDED });
575
+ });
576
+ // 卖家提交证据 → 延期 24h;状态保持 open
577
+ app.post('/api/claim-tasks/:id/seller-evidence', (req, res) => {
578
+ const user = auth(req, res);
579
+ if (!user)
580
+ return;
581
+ const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?')
582
+ .get(req.params.id);
583
+ if (!task)
584
+ return void res.status(404).json({ error: '任务不存在' });
585
+ if (task.seller_id !== user.id)
586
+ return void res.status(403).json({ error: '仅订单卖家可提交证据' });
587
+ if (task.status !== 'open')
588
+ return void res.status(400).json({ error: '任务非 open 状态,不接受证据' });
589
+ if (task.seller_evidence_at)
590
+ return void res.status(409).json({ error: '已提交过证据' });
591
+ const evidence_uri = String(req.body?.evidence_uri || '').trim();
592
+ if (!evidence_uri || evidence_uri.length < 4 || evidence_uri.length > 500) {
593
+ return void res.status(400).json({ error: 'evidence_uri 长度需 4-500' });
594
+ }
595
+ const oldDeadline = new Date(String(task.deadline_at)).getTime();
596
+ const newCandidate = Date.now() + CLAIM_SELLER_EXTENSION_HOURS * 3600_000;
597
+ const newDeadline = new Date(Math.max(oldDeadline, newCandidate)).toISOString();
598
+ db.prepare(`UPDATE claim_verification_tasks
599
+ SET seller_evidence_uri = ?, seller_evidence_at = datetime('now'), deadline_at = ?
600
+ WHERE id = ?`).run(evidence_uri, newDeadline, req.params.id);
601
+ try {
602
+ const productTitle = db.prepare('SELECT title FROM products WHERE id = ?').get(task.product_id)?.title || '—';
603
+ const claimLabel = CLAIM_TARGET_LABEL_ZH[String(task.claim_target)] || String(task.claim_target);
604
+ notifyEligibleVerifiers(db, generateId, {
605
+ taskId: String(task.id), productTitle, claimTargetLabel: claimLabel,
606
+ buyerId: task.buyer_id, sellerId: task.seller_id,
607
+ notificationType: 'claim_evidence_added',
608
+ });
609
+ }
610
+ catch (e) {
611
+ console.error('[V2 seller evidence notify]', e.message);
612
+ }
613
+ res.json({ success: true, deadline_at: newDeadline, warning: '虚假证据将在结算时扣除 20% stake(M7.3b)' });
614
+ });
615
+ }