@seasonkoh/webaz 0.1.23 → 0.1.25

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 (187) hide show
  1. package/README.md +2 -0
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +198 -83
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -1,4 +1,5 @@
1
1
  import { generateAnchor, lookupAnchor, retireAnchor, userReferralVolume, computeTierLetter, userAnchorQuotaStats, TIER_THRESHOLDS, ANCHOR_HANDLE_MAX_FOR_USE, } from '../../layer2-business/L2-anchor-registry/anchor-registry.js';
2
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
3
  export function registerAnchorsRoutes(app, deps) {
3
4
  const { db, auth, rateLimitOk } = deps;
4
5
  // POST /api/anchor/generate
@@ -25,7 +26,7 @@ export function registerAnchorsRoutes(app, deps) {
25
26
  res.json({ ok: true, anchor: r.anchor, tier_letter: r.tier_letter });
26
27
  });
27
28
  // GET /api/anchor/:code/lookup — 公开(无需 auth)
28
- app.get('/api/anchor/:code/lookup', (req, res) => {
29
+ app.get('/api/anchor/:code/lookup', async (req, res) => {
29
30
  if (!rateLimitOk(req.ip || 'anon', 60, 60_000))
30
31
  return void res.status(429).json({ error: 'too_many_lookups' });
31
32
  const r = lookupAnchor(db, String(req.params.code || ''));
@@ -38,15 +39,15 @@ export function registerAnchorsRoutes(app, deps) {
38
39
  return void res.status(404).json({ found: false, hint: 'reclaimable' });
39
40
  }
40
41
  // 2026-05-24 富化响应:附 owner 详情 + 商品推荐指数
41
- const owner = db.prepare(`
42
+ const owner = await dbOne(`
42
43
  SELECT u.name, u.handle, u.region, u.created_at, u.bio,
43
44
  (SELECT COUNT(*) FROM follows WHERE followee_id = u.id) as follower_count,
44
45
  (SELECT COALESCE(SUM(s.like_count), 0) FROM shareables s WHERE s.owner_id = u.id AND s.status = 'active') as total_likes_received
45
46
  FROM users u WHERE u.id = ? AND u.id != 'sys_protocol'
46
- `).get(r.owner_id);
47
+ `, [r.owner_id]);
47
48
  let product = null;
48
49
  if (r.target_kind === 'product') {
49
- product = db.prepare(`
50
+ product = (await dbOne(`
50
51
  SELECT p.id, p.title, p.price, p.category, p.images, p.completion_count, p.total_likes,
51
52
  (SELECT COUNT(DISTINCT buyer_id) FROM order_ratings rt WHERE rt.product_id = p.id AND rt.stars >= 4) as recommend_count,
52
53
  (SELECT ROUND(AVG(stars), 2) FROM order_ratings rt WHERE rt.product_id = p.id) as avg_rating,
@@ -54,12 +55,12 @@ export function registerAnchorsRoutes(app, deps) {
54
55
  u.handle as seller_handle, u.name as seller_name
55
56
  FROM products p LEFT JOIN users u ON u.id = p.seller_id
56
57
  WHERE p.id = ? AND p.status = 'active'
57
- `).get(r.target_id);
58
+ `, [r.target_id])) ?? null;
58
59
  }
59
60
  else if (r.target_kind === 'shareable') {
60
- const sh = db.prepare(`SELECT related_product_id FROM shareables WHERE id = ?`).get(r.target_id);
61
+ const sh = await dbOne(`SELECT related_product_id FROM shareables WHERE id = ?`, [r.target_id]);
61
62
  if (sh?.related_product_id) {
62
- product = db.prepare(`
63
+ product = (await dbOne(`
63
64
  SELECT p.id, p.title, p.price, p.category, p.images, p.completion_count, p.total_likes,
64
65
  (SELECT COUNT(DISTINCT buyer_id) FROM order_ratings rt WHERE rt.product_id = p.id AND rt.stars >= 4) as recommend_count,
65
66
  (SELECT ROUND(AVG(stars), 2) FROM order_ratings rt WHERE rt.product_id = p.id) as avg_rating,
@@ -67,7 +68,7 @@ export function registerAnchorsRoutes(app, deps) {
67
68
  u.handle as seller_handle, u.name as seller_name
68
69
  FROM products p LEFT JOIN users u ON u.id = p.seller_id
69
70
  WHERE p.id = ? AND p.status = 'active'
70
- `).get(sh.related_product_id);
71
+ `, [sh.related_product_id])) ?? null;
71
72
  }
72
73
  }
73
74
  res.json({
@@ -81,7 +82,7 @@ export function registerAnchorsRoutes(app, deps) {
81
82
  });
82
83
  });
83
84
  // POST /api/anchor/:code/touch — 写 attribution(first-touch + 30d)
84
- app.post('/api/anchor/:code/touch', (req, res) => {
85
+ app.post('/api/anchor/:code/touch', async (req, res) => {
85
86
  const user = auth(req, res);
86
87
  if (!user)
87
88
  return;
@@ -93,27 +94,25 @@ export function registerAnchorsRoutes(app, deps) {
93
94
  let attributedProducts = 0;
94
95
  const expiresAt = new Date(Date.now() + 30 * 86400_000).toISOString().slice(0, 19).replace('T', ' ');
95
96
  if (r.target_kind === 'product') {
96
- const existing = db.prepare(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`).get(r.target_id, user.id);
97
+ const existing = await dbOne(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`, [r.target_id, user.id]);
97
98
  if (!existing) {
98
- db.prepare(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,NULL,?)`)
99
- .run(r.target_id, user.id, r.owner_id, expiresAt);
99
+ await dbRun(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,NULL,?)`, [r.target_id, user.id, r.owner_id, expiresAt]);
100
100
  attributedProducts = 1;
101
101
  }
102
102
  }
103
103
  else if (r.target_kind === 'shareable') {
104
- const s = db.prepare(`SELECT id, related_product_id FROM shareables WHERE id = ?`).get(r.target_id);
104
+ const s = await dbOne(`SELECT id, related_product_id FROM shareables WHERE id = ?`, [r.target_id]);
105
105
  if (s?.related_product_id) {
106
- const existing = db.prepare(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`).get(s.related_product_id, user.id);
106
+ const existing = await dbOne(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`, [s.related_product_id, user.id]);
107
107
  if (!existing) {
108
- db.prepare(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,?,?)`)
109
- .run(s.related_product_id, user.id, r.owner_id, s.id, expiresAt);
108
+ await dbRun(`INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at) VALUES (?,?,?,?,?)`, [s.related_product_id, user.id, r.owner_id, s.id, expiresAt]);
110
109
  attributedProducts = 1;
111
110
  }
112
111
  }
113
112
  }
114
113
  else if (r.target_kind === 'user') {
115
114
  // 限 LIMIT 50 防 DoS
116
- const ownerProducts = db.prepare(`SELECT id FROM products WHERE seller_id = ? AND status = 'active' ORDER BY last_sold_at DESC NULLS LAST LIMIT 50`).all(r.owner_id);
115
+ const ownerProducts = await dbAll(`SELECT id FROM products WHERE seller_id = ? AND status = 'active' ORDER BY last_sold_at DESC NULLS LAST LIMIT 50`, [r.owner_id]);
117
116
  db.transaction(() => {
118
117
  for (const p of ownerProducts) {
119
118
  const existing = db.prepare(`SELECT 1 FROM product_share_attribution WHERE product_id = ? AND recipient_id = ?`).get(p.id, user.id);
@@ -146,14 +145,14 @@ export function registerAnchorsRoutes(app, deps) {
146
145
  }
147
146
  res.json({ ok: true });
148
147
  });
149
- app.get('/api/anchor/me', (req, res) => {
148
+ app.get('/api/anchor/me', async (req, res) => {
150
149
  const user = auth(req, res);
151
150
  if (!user)
152
151
  return;
153
- const rows = db.prepare(`
152
+ const rows = await dbAll(`
154
153
  SELECT anchor, prefix, middle, tier_letter, target_kind, target_id, status, retired_at, hits, last_hit_at, created_at
155
154
  FROM anchor_registry WHERE owner_id = ? ORDER BY created_at DESC LIMIT 100
156
- `).all(user.id);
155
+ `, [user.id]);
157
156
  const vol = userReferralVolume(db, user.id);
158
157
  const tier = computeTierLetter(vol);
159
158
  const quota = userAnchorQuotaStats(db, user.id);
@@ -1,6 +1,8 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerAnnouncementsRoutes(app, deps) {
2
- const { db, generateId, auth, safeRoles, requireProtocolAdmin, isRootAdmin, getAdminScope, logAdminAction } = deps;
3
- app.post('/api/admin/announcements', (req, res) => {
3
+ // db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
+ const { generateId, auth, safeRoles, requireProtocolAdmin, isRootAdmin, getAdminScope, logAdminAction } = deps;
5
+ app.post('/api/admin/announcements', async (req, res) => {
4
6
  const admin = requireProtocolAdmin(req, res);
5
7
  if (!admin)
6
8
  return;
@@ -26,16 +28,15 @@ export function registerAnnouncementsRoutes(app, deps) {
26
28
  }
27
29
  }
28
30
  const id = generateId('ann');
29
- db.prepare(`INSERT INTO announcements (id, author_id, title, body, target_roles, target_regions, severity, starts_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?)`)
30
- .run(id, admin.id, title.trim(), body.trim(), rolesJson, regionsJson, severity || 'info', starts_at || null, expires_at || null);
31
+ await dbRun(`INSERT INTO announcements (id, author_id, title, body, target_roles, target_regions, severity, starts_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?)`, [id, admin.id, title.trim(), body.trim(), rolesJson, regionsJson, severity || 'info', starts_at || null, expires_at || null]);
31
32
  logAdminAction(admin.id, 'create_announcement', 'announcement', id, { title, severity: severity || 'info' });
32
33
  res.json({ success: true, id });
33
34
  });
34
- app.patch('/api/admin/announcements/:id', (req, res) => {
35
+ app.patch('/api/admin/announcements/:id', async (req, res) => {
35
36
  const admin = requireProtocolAdmin(req, res);
36
37
  if (!admin)
37
38
  return;
38
- const ann = db.prepare('SELECT id, author_id FROM announcements WHERE id = ?').get(req.params.id);
39
+ const ann = await dbOne('SELECT id, author_id FROM announcements WHERE id = ?', [req.params.id]);
39
40
  if (!ann)
40
41
  return void res.status(404).json({ error: '公告不存在' });
41
42
  if (!isRootAdmin(admin) && ann.author_id !== admin.id)
@@ -54,17 +55,17 @@ export function registerAnnouncementsRoutes(app, deps) {
54
55
  if (sets.length === 0)
55
56
  return void res.status(400).json({ error: '无可更新字段' });
56
57
  args.push(req.params.id);
57
- db.prepare(`UPDATE announcements SET ${sets.join(', ')} WHERE id = ?`).run(...args);
58
+ await dbRun(`UPDATE announcements SET ${sets.join(', ')} WHERE id = ?`, args);
58
59
  res.json({ success: true });
59
60
  });
60
61
  // 列出对当前用户可见的活跃公告(按角色 + 区域过滤)
61
- app.get('/api/announcements/active', (req, res) => {
62
+ app.get('/api/announcements/active', async (req, res) => {
62
63
  const user = auth(req, res);
63
64
  if (!user)
64
65
  return;
65
66
  const userRoles = safeRoles(user);
66
67
  const userRegion = user.region || 'global';
67
- const rows = db.prepare(`
68
+ const rows = await dbAll(`
68
69
  SELECT a.id, a.title, a.body, a.severity, a.created_at, a.target_roles, a.target_regions,
69
70
  (SELECT 1 FROM announcement_reads WHERE user_id = ? AND announcement_id = a.id) as is_read
70
71
  FROM announcements a
@@ -72,7 +73,7 @@ export function registerAnnouncementsRoutes(app, deps) {
72
73
  AND (a.starts_at IS NULL OR a.starts_at <= datetime('now'))
73
74
  AND (a.expires_at IS NULL OR a.expires_at >= datetime('now'))
74
75
  ORDER BY a.created_at DESC LIMIT 50
75
- `).all(user.id);
76
+ `, [user.id]);
76
77
  // JS 端 filter 角色 / 区域(避免 JSON LIKE 在 SQLite 中麻烦)
77
78
  const filtered = rows.filter(a => {
78
79
  if (a.target_roles) {
@@ -96,13 +97,12 @@ export function registerAnnouncementsRoutes(app, deps) {
96
97
  }).map(a => ({ ...a, target_roles: undefined, target_regions: undefined, is_read: !!a.is_read }));
97
98
  res.json({ items: filtered });
98
99
  });
99
- app.post('/api/announcements/:id/read', (req, res) => {
100
+ app.post('/api/announcements/:id/read', async (req, res) => {
100
101
  const user = auth(req, res);
101
102
  if (!user)
102
103
  return;
103
104
  try {
104
- db.prepare(`INSERT OR IGNORE INTO announcement_reads (user_id, announcement_id) VALUES (?,?)`)
105
- .run(user.id, req.params.id);
105
+ await dbRun(`INSERT OR IGNORE INTO announcement_reads (user_id, announcement_id) VALUES (?,?)`, [user.id, req.params.id]);
106
106
  }
107
107
  catch { }
108
108
  res.json({ success: true });
@@ -1,4 +1,7 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerArbitratorRoutes(app, deps) {
3
+ // 只读/单写站点走 RFC-016 异步 seam;db 保留:apply/withdraw/approve/reject 是
4
+ // stake 资金路径,状态翻转 + 钱包扣/退必须原子(db.transaction + CAS),Phase 3 迁 pg 行锁。
2
5
  const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
3
6
  app.get('/api/arbitrator/eligibility', (req, res) => {
4
7
  const user = auth(req, res);
@@ -12,7 +15,7 @@ export function registerArbitratorRoutes(app, deps) {
12
15
  return;
13
16
  res.json(getArbitratorState(user.id));
14
17
  });
15
- app.post('/api/arbitrator/apply', (req, res) => {
18
+ app.post('/api/arbitrator/apply', async (req, res) => {
16
19
  const user = auth(req, res);
17
20
  if (!user)
18
21
  return;
@@ -20,13 +23,13 @@ export function registerArbitratorRoutes(app, deps) {
20
23
  return void errorRes(res, 403, 'ROLE_NOT_BUYER', '外部仲裁员仅 buyer 角色可申请(卖家 / 受信角色请联系管理员)');
21
24
  }
22
25
  const userId = user.id;
23
- const wl = db.prepare("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?").get(userId);
26
+ const wl = await dbOne("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?", [userId]);
24
27
  if (wl)
25
28
  return void res.json({ error: '你已经是仲裁员,无需重新申请' });
26
- const pending = db.prepare("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'").get(userId);
29
+ const pending = await dbOne("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'", [userId]);
27
30
  if (pending)
28
31
  return void res.json({ error: '你已有待审申请' });
29
- const lastReject = db.prepare("SELECT reviewed_at FROM arbitrator_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1").get(userId);
32
+ const lastReject = await dbOne("SELECT reviewed_at FROM arbitrator_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1", [userId]);
30
33
  if (lastReject?.reviewed_at) {
31
34
  const cooldownEnd = new Date(new Date(lastReject.reviewed_at).getTime() + ARB_APP_REJECT_COOLDOWN_DAYS * 86400_000);
32
35
  if (cooldownEnd > new Date()) {
@@ -36,80 +39,143 @@ export function registerArbitratorRoutes(app, deps) {
36
39
  const elig = checkArbitratorEligibility(userId);
37
40
  if (!elig.eligible)
38
41
  return void res.json({ error: '信誉指标未达标', eligibility: elig });
42
+ // 友好预检查(读):真正的守恒门在事务内(WHERE balance >= stake)。
39
43
  if (ARB_STAKE_REQUIRED > 0) {
40
- const wallet = db.prepare("SELECT balance, staked FROM wallets WHERE user_id = ?").get(userId);
44
+ const wallet = await dbOne("SELECT balance FROM wallets WHERE user_id = ?", [userId]);
41
45
  if (!wallet || wallet.balance < ARB_STAKE_REQUIRED) {
42
46
  return void res.json({ error: `质押需 ${ARB_STAKE_REQUIRED} WAZ,钱包余额不足` });
43
47
  }
44
- db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?")
45
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
46
48
  }
47
- db.prepare("INSERT INTO arbitrator_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)")
48
- .run(generateId('aapp'), userId, 'pending', JSON.stringify(elig.items));
49
+ const appId = generateId('aapp');
50
+ // stake 原子段:重检 whitelist/pending(防并发双申请双质押)+ 钱包扣减(守恒 guard)+ INSERT 申请
51
+ try {
52
+ db.transaction(() => {
53
+ if (db.prepare("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?").get(userId))
54
+ throw new Error('ARB_ALREADY');
55
+ if (db.prepare("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'").get(userId))
56
+ throw new Error('ARB_PENDING');
57
+ if (ARB_STAKE_REQUIRED > 0) {
58
+ const debit = db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?")
59
+ .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId, ARB_STAKE_REQUIRED);
60
+ if (debit.changes === 0)
61
+ throw new Error('ARB_INSUFFICIENT');
62
+ }
63
+ db.prepare("INSERT INTO arbitrator_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)")
64
+ .run(appId, userId, 'pending', JSON.stringify(elig.items));
65
+ })();
66
+ }
67
+ catch (e) {
68
+ const msg = e.message;
69
+ if (msg === 'ARB_ALREADY')
70
+ return void res.json({ error: '你已经是仲裁员,无需重新申请' });
71
+ if (msg === 'ARB_PENDING')
72
+ return void res.json({ error: '你已有待审申请' });
73
+ if (msg === 'ARB_INSUFFICIENT')
74
+ return void res.json({ error: `质押需 ${ARB_STAKE_REQUIRED} WAZ,钱包余额不足` });
75
+ console.error('[arbitrator apply tx]', msg);
76
+ return void res.status(500).json({ error: '申请失败,请重试' });
77
+ }
49
78
  res.json({ success: true, stake_locked: ARB_STAKE_REQUIRED });
50
79
  });
51
- app.post('/api/arbitrator/withdraw-application', (req, res) => {
80
+ app.post('/api/arbitrator/withdraw-application', async (req, res) => {
52
81
  const user = auth(req, res);
53
82
  if (!user)
54
83
  return;
55
84
  const userId = user.id;
56
- const pending = db.prepare("SELECT id FROM arbitrator_applications WHERE user_id = ? AND status = 'pending' LIMIT 1").get(userId);
85
+ const pending = await dbOne("SELECT id FROM arbitrator_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [userId]);
57
86
  if (!pending)
58
87
  return void res.json({ error: '没有待审申请' });
59
- db.prepare("UPDATE arbitrator_applications SET status='withdrawn', reviewed_at=datetime('now') WHERE id = ?").run(pending.id);
60
- if (ARB_STAKE_REQUIRED > 0) {
61
- db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
62
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
88
+ // 原子段:CAS 翻转 pending→withdrawn(防并发/admin 抢跑双退)+ 退质押仅在本请求真翻转时
89
+ try {
90
+ db.transaction(() => {
91
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='withdrawn', reviewed_at=datetime('now') WHERE id = ? AND status = 'pending'").run(pending.id);
92
+ if (cas.changes === 0)
93
+ throw new Error('ARB_RACE');
94
+ if (ARB_STAKE_REQUIRED > 0) {
95
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
96
+ }
97
+ })();
98
+ }
99
+ catch (e) {
100
+ if (e.message === 'ARB_RACE')
101
+ return void res.status(409).json({ error: '申请状态已变化,请刷新' });
102
+ console.error('[arbitrator withdraw tx]', e.message);
103
+ return void res.status(500).json({ error: '撤回失败,请重试' });
63
104
  }
64
105
  res.json({ success: true });
65
106
  });
66
107
  // Admin
67
- app.get('/api/admin/arbitrator-applications', (req, res) => {
108
+ app.get('/api/admin/arbitrator-applications', async (req, res) => {
68
109
  const admin = requireArbitrationAdmin(req, res);
69
110
  if (!admin)
70
111
  return;
71
112
  const status = String(req.query.status || 'pending');
72
- const items = db.prepare(`
113
+ const items = await dbAll(`
73
114
  SELECT aa.*, u.name as user_name, u.handle, u.region
74
115
  FROM arbitrator_applications aa
75
116
  JOIN users u ON u.id = aa.user_id
76
117
  WHERE aa.status = ?
77
118
  ORDER BY aa.applied_at DESC LIMIT 100
78
- `).all(status);
119
+ `, [status]);
79
120
  res.json({ items });
80
121
  });
81
- app.post('/api/admin/arbitrator-applications/:id/approve', (req, res) => {
122
+ app.post('/api/admin/arbitrator-applications/:id/approve', async (req, res) => {
82
123
  const admin = requireArbitrationAdmin(req, res);
83
124
  if (!admin)
84
125
  return;
85
126
  const { note } = req.body;
86
- const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
127
+ const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
87
128
  if (!appRow)
88
129
  return void res.json({ error: '申请不存在' });
89
130
  if (appRow.status !== 'pending')
90
131
  return void res.json({ error: '该申请不在待审状态' });
91
- db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
92
- .run(admin.id, note || null, appRow.id);
93
- db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
94
- .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
132
+ // 原子段:CAS 翻转 pending→approved + 入白名单仅在本请求真翻转时(防并发双批准)
133
+ try {
134
+ db.transaction(() => {
135
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
136
+ .run(admin.id, note || null, appRow.id);
137
+ if (cas.changes === 0)
138
+ throw new Error('ARB_RACE');
139
+ db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
140
+ .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
141
+ })();
142
+ }
143
+ catch (e) {
144
+ if (e.message === 'ARB_RACE')
145
+ return void res.json({ error: '该申请不在待审状态' });
146
+ console.error('[arbitrator approve tx]', e.message);
147
+ return void res.status(500).json({ error: '批准失败,请重试' });
148
+ }
95
149
  logAdminAction(admin.id, 'approve_arbitrator', 'user', appRow.user_id, { note });
96
150
  res.json({ success: true });
97
151
  });
98
- app.post('/api/admin/arbitrator-applications/:id/reject', (req, res) => {
152
+ app.post('/api/admin/arbitrator-applications/:id/reject', async (req, res) => {
99
153
  const admin = requireArbitrationAdmin(req, res);
100
154
  if (!admin)
101
155
  return;
102
156
  const { note } = req.body;
103
- const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
157
+ const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
104
158
  if (!appRow)
105
159
  return void res.json({ error: '申请不存在' });
106
160
  if (appRow.status !== 'pending')
107
161
  return void res.json({ error: '该申请不在待审状态' });
108
- db.prepare("UPDATE arbitrator_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
109
- .run(admin.id, note || null, appRow.id);
110
- if (ARB_STAKE_REQUIRED > 0) {
111
- db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
112
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, appRow.user_id);
162
+ // 原子段:CAS 翻转 pending→rejected + 退质押仅在本请求真翻转时(防并发双拒双退)
163
+ try {
164
+ db.transaction(() => {
165
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
166
+ .run(admin.id, note || null, appRow.id);
167
+ if (cas.changes === 0)
168
+ throw new Error('ARB_RACE');
169
+ if (ARB_STAKE_REQUIRED > 0) {
170
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, appRow.user_id);
171
+ }
172
+ })();
173
+ }
174
+ catch (e) {
175
+ if (e.message === 'ARB_RACE')
176
+ return void res.json({ error: '该申请不在待审状态' });
177
+ console.error('[arbitrator reject tx]', e.message);
178
+ return void res.status(500).json({ error: '拒绝失败,请重试' });
113
179
  }
114
180
  logAdminAction(admin.id, 'reject_arbitrator', 'user', appRow.user_id, { note });
115
181
  res.json({ success: true });