@seasonkoh/webaz 0.1.24 → 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 +165 -64
  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,61 +1,63 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerPromoterRoutes(app, deps) {
2
3
  const { db, auth, isAllowedSponsor } = deps;
3
- app.get('/api/promoter/dashboard', (req, res) => {
4
+ void db; // RFC-016: 本文件已全量走异步 seam;db 仍在 deps 由调用方注入,此处不直接使用
5
+ app.get('/api/promoter/dashboard', async (req, res) => {
4
6
  const user = auth(req, res);
5
7
  if (!user)
6
8
  return;
7
9
  const userId = user.id;
8
- const l1 = db.prepare("SELECT COUNT(*) as n FROM users WHERE sponsor_id = ?").get(userId).n;
9
- const l2 = db.prepare(`
10
+ const l1 = (await dbOne("SELECT COUNT(*) as n FROM users WHERE sponsor_id = ?", [userId])).n;
11
+ const l2 = (await dbOne(`
10
12
  SELECT COUNT(*) as n FROM users
11
13
  WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
12
- `).get(userId).n;
13
- const l3 = db.prepare(`
14
+ `, [userId])).n;
15
+ const l3 = (await dbOne(`
14
16
  SELECT COUNT(*) as n FROM users
15
17
  WHERE sponsor_id IN (
16
18
  SELECT id FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
17
19
  )
18
- `).get(userId).n;
19
- const earned = db.prepare(`
20
+ `, [userId])).n;
21
+ const earned = await dbAll(`
20
22
  SELECT level, COUNT(*) as orders, COALESCE(SUM(amount),0) as total
21
23
  FROM commission_records WHERE beneficiary_id = ?
22
24
  GROUP BY level
23
- `).all(userId);
25
+ `, [userId]);
24
26
  const byLevel = { 1: { orders: 0, total: 0 }, 2: { orders: 0, total: 0 }, 3: { orders: 0, total: 0 } };
25
27
  for (const r of earned)
26
28
  byLevel[r.level] = { orders: r.orders, total: r.total };
27
29
  const grand = byLevel[1].total + byLevel[2].total + byLevel[3].total;
28
- const recent = db.prepare(`
30
+ const recent = await dbAll(`
29
31
  SELECT cr.id, cr.order_id, cr.level, cr.amount, cr.rate, cr.created_at,
30
32
  u.name as source_buyer_name
31
33
  FROM commission_records cr
32
34
  LEFT JOIN users u ON u.id = cr.source_buyer_id
33
35
  WHERE cr.beneficiary_id = ?
34
36
  ORDER BY cr.created_at DESC LIMIT 20
35
- `).all(userId);
36
- const me = db.prepare("SELECT sponsor_id, sponsor_path, region FROM users WHERE id = ?").get(userId);
37
- const mySponsor = me?.sponsor_id ? db.prepare("SELECT name FROM users WHERE id = ?").get(me.sponsor_id) : null;
38
- const myUser = db.prepare("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?").get(userId);
39
- const leftChildName = myUser?.left_child_id ? db.prepare("SELECT name FROM users WHERE id = ?").get(myUser.left_child_id)?.name : null;
40
- const rightChildName = myUser?.right_child_id ? db.prepare("SELECT name FROM users WHERE id = ?").get(myUser.right_child_id)?.name : null;
41
- const myPlacementName = myUser?.placement_id ? db.prepare("SELECT name FROM users WHERE id = ?").get(myUser.placement_id)?.name : null;
42
- const scoreAgg = db.prepare(`
37
+ `, [userId]);
38
+ const me = (await dbOne("SELECT sponsor_id, sponsor_path, region FROM users WHERE id = ?", [userId]));
39
+ const mySponsor = me?.sponsor_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [me.sponsor_id])) : null;
40
+ const myUser = await dbOne("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?", [userId]);
41
+ const leftChildName = myUser?.left_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.left_child_id]))?.name : null;
42
+ const rightChildName = myUser?.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.right_child_id]))?.name : null;
43
+ const myPlacementName = myUser?.placement_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.placement_id]))?.name : null;
44
+ const scoreAgg = (await dbOne(`
43
45
  SELECT
44
46
  COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending_score,
45
47
  COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz,
46
48
  COUNT(*) as total_hits
47
49
  FROM binary_score_records WHERE user_id = ?
48
- `).get(userId);
49
- const recentBinary = db.prepare(`
50
+ `, [userId]));
51
+ const recentBinary = await dbAll(`
50
52
  SELECT id, tier, score, settled_at, waz_amount, created_at
51
53
  FROM binary_score_records WHERE user_id = ?
52
54
  ORDER BY created_at DESC LIMIT 10
53
- `).all(userId);
54
- const tiers = db.prepare("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC").all();
55
+ `, [userId]);
56
+ const tiers = await dbAll("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC");
55
57
  const canL1Share = isAllowedSponsor(userId);
56
- const completedOrders = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
57
- const overrideRow = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId);
58
- const shareableProducts = db.prepare(`
58
+ const completedOrders = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
59
+ const overrideRow = await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]);
60
+ const shareableProducts = await dbAll(`
59
61
  SELECT p.id, p.title, p.price, p.category, p.commission_rate,
60
62
  (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as total_sales,
61
63
  COALESCE((SELECT SUM(cr.amount) FROM commission_records cr
@@ -66,20 +68,20 @@ export function registerPromoterRoutes(app, deps) {
66
68
  AND p.commission_rate IS NOT NULL AND p.commission_rate > 0
67
69
  AND p.status = 'active'
68
70
  ORDER BY my_earned DESC, total_sales DESC LIMIT 20
69
- `).all(userId, userId);
70
- const earnedLast30 = db.prepare(`
71
+ `, [userId, userId]);
72
+ const earnedLast30 = (await dbOne(`
71
73
  SELECT COALESCE(SUM(amount),0) as total FROM commission_records
72
74
  WHERE beneficiary_id = ? AND created_at >= datetime('now','-30 days')
73
- `).get(userId).total;
74
- const earnedPrev30 = db.prepare(`
75
+ `, [userId])).total;
76
+ const earnedPrev30 = (await dbOne(`
75
77
  SELECT COALESCE(SUM(amount),0) as total FROM commission_records
76
78
  WHERE beneficiary_id = ? AND created_at >= datetime('now','-60 days')
77
79
  AND created_at < datetime('now','-30 days')
78
- `).get(userId).total;
79
- const wazLast30 = db.prepare(`
80
+ `, [userId])).total;
81
+ const wazLast30 = (await dbOne(`
80
82
  SELECT COALESCE(SUM(waz_amount),0) as total FROM binary_score_records
81
83
  WHERE user_id = ? AND settled_at >= datetime('now','-30 days')
82
- `).get(userId).total;
84
+ `, [userId])).total;
83
85
  const projection = {
84
86
  last_30_commission: earnedLast30,
85
87
  prev_30_commission: earnedPrev30,
@@ -99,7 +101,7 @@ export function registerPromoterRoutes(app, deps) {
99
101
  else
100
102
  insights.push({ type: 'balanced', level: 'success', text: `双腿均衡度 ${(ratio * 100).toFixed(0)}% — 对碰节奏健康` });
101
103
  }
102
- const lastInvite = db.prepare(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`).get(userId);
104
+ const lastInvite = (await dbOne(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`, [userId]));
103
105
  if (lastInvite.t) {
104
106
  const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
105
107
  if (days > 14)
@@ -121,10 +123,10 @@ export function registerPromoterRoutes(app, deps) {
121
123
  if (shareableProducts.length > 0 && grand === 0) {
122
124
  insights.push({ type: 'first_share', level: 'info', text: `你有 ${shareableProducts.length} 个可分享商品但暂无成交 — 试着把链接发给身边的人` });
123
125
  }
124
- const treeNode = (uid) => {
126
+ const treeNode = async (uid) => {
125
127
  if (!uid)
126
128
  return null;
127
- const u = db.prepare("SELECT id, name, total_left_pv, total_right_pv, left_child_id, right_child_id FROM users WHERE id = ?").get(uid);
129
+ const u = await dbOne("SELECT id, name, total_left_pv, total_right_pv, left_child_id, right_child_id FROM users WHERE id = ?", [uid]);
128
130
  if (!u)
129
131
  return null;
130
132
  return {
@@ -135,25 +137,29 @@ export function registerPromoterRoutes(app, deps) {
135
137
  right_id: u.right_child_id ?? null,
136
138
  };
137
139
  };
138
- const me_node = treeNode(userId);
139
- const left_node = treeNode(myUser?.left_child_id);
140
- const right_node = treeNode(myUser?.right_child_id);
140
+ const me_node = await treeNode(userId);
141
+ const left_node = await treeNode(myUser?.left_child_id);
142
+ const right_node = await treeNode(myUser?.right_child_id);
141
143
  const binaryTree = {
142
144
  me: me_node,
143
145
  left: left_node,
144
146
  right: right_node,
145
- ll: treeNode(left_node?.left_id),
146
- lr: treeNode(left_node?.right_id),
147
- rl: treeNode(right_node?.left_id),
148
- rr: treeNode(right_node?.right_id),
147
+ ll: await treeNode(left_node?.left_id),
148
+ lr: await treeNode(left_node?.right_id),
149
+ rl: await treeNode(right_node?.left_id),
150
+ rr: await treeNode(right_node?.right_id),
149
151
  };
150
- const meCard = db.prepare("SELECT permanent_code, handle FROM users WHERE id = ?").get(userId);
151
- const codeForLink = meCard?.permanent_code || userId;
152
+ const meCard = await dbOne("SELECT permanent_code, handle FROM users WHERE id = ?", [userId]);
153
+ // invite links use permanent_code ONLY — never fall back to user_id (would leak usr_xxx into ?ref).
154
+ const codeForLink = meCard?.permanent_code || null;
155
+ const host = `${req.protocol}://${req.get('host')}`;
152
156
  res.json({
153
157
  user_id: userId,
154
158
  permanent_code: meCard?.permanent_code || null,
155
159
  handle: meCard?.handle || null,
156
- referral_link: `${req.protocol}://${req.get('host')}/i/${codeForLink}`,
160
+ invite_code_available: !!codeForLink,
161
+ referral_link: codeForLink ? `${host}/i/${codeForLink}` : null,
162
+ invite_unavailable_reason: codeForLink ? null : 'permanent_code_missing — refresh or contact support',
157
163
  region: me?.region || 'global',
158
164
  my_sponsor: mySponsor ? { id: me.sponsor_id, name: mySponsor.name } : null,
159
165
  permissions: {
@@ -176,8 +182,8 @@ export function registerPromoterRoutes(app, deps) {
176
182
  projection,
177
183
  insights,
178
184
  atomic: {
179
- left_invite_url: `${req.protocol}://${req.get('host')}/i/${codeForLink}-L`,
180
- right_invite_url: `${req.protocol}://${req.get('host')}/i/${codeForLink}-R`,
185
+ left_invite_url: codeForLink ? `${host}/i/${codeForLink}-L` : null,
186
+ right_invite_url: codeForLink ? `${host}/i/${codeForLink}-R` : null,
181
187
  total_left_pv: Number(myUser?.total_left_pv ?? 0),
182
188
  total_right_pv: Number(myUser?.total_right_pv ?? 0),
183
189
  left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
@@ -191,18 +197,18 @@ export function registerPromoterRoutes(app, deps) {
191
197
  });
192
198
  });
193
199
  // 直推 L1 列表
194
- app.get('/api/promoter/team', (req, res) => {
200
+ app.get('/api/promoter/team', async (req, res) => {
195
201
  const user = auth(req, res);
196
202
  if (!user)
197
203
  return;
198
204
  const userId = user.id;
199
- const rows = db.prepare(`
205
+ const rows = await dbAll(`
200
206
  SELECT u.id, u.name, u.created_at, u.region,
201
207
  (SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as their_l1,
202
208
  COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = ? AND source_buyer_id = u.id), 0) as my_earned_from_them
203
209
  FROM users u WHERE u.sponsor_id = ?
204
210
  ORDER BY u.created_at DESC LIMIT 100
205
- `).all(userId, userId);
211
+ `, [userId, userId]);
206
212
  res.json({ team: rows });
207
213
  });
208
214
  }
@@ -0,0 +1,19 @@
1
+ import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
2
+ export function registerPublicBuildTasksRoutes(app, deps) {
3
+ const { db, errorRes } = deps;
4
+ app.get('/api/public/build-tasks', (req, res) => {
5
+ const v = validateTaskFilters(req.query);
6
+ if (!v.ok)
7
+ return void errorRes(res, 400, v.code, v.detail); // fail-closed: bad filter → typed 400
8
+ const tasks = listBuildTasksWithAgentMetadata(db, v.filters, 'public');
9
+ res.json(withContributionReadEnvelope({ tasks }));
10
+ });
11
+ app.get('/api/public/build-tasks/:id', (req, res) => {
12
+ // 'public' scope already restricts to audience=public + status=open; a non-visible task returns null →
13
+ // 404 (same as truly-missing, so existence of a restricted/internal task is never disclosed).
14
+ const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'public');
15
+ if (!task)
16
+ return void errorRes(res, 404, 'NOT_FOUND', '任务不存在');
17
+ res.json(withContributionReadEnvelope({ task }));
18
+ });
19
+ }
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { SOFTWARE_VERSION, CONTRACT_VERSION } from '../../version.js';
5
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
5
6
  import { capabilityMatrix } from '../endpoint-actions.js';
6
7
  import { buildEntityDictionary } from '../entity-dictionary.js';
7
8
  import { buildGoalIndex } from '../goal-index.js';
@@ -13,13 +14,13 @@ import { buildNegativeSpace } from '../negative-space.js';
13
14
  import { buildAcpProductFeed } from '../acp-feed.js';
14
15
  export function registerPublicUtilsRoutes(app, deps) {
15
16
  const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
16
- app.get('/api/health', (_req, res) => {
17
+ app.get('/api/health', async (_req, res) => {
17
18
  const t0 = Date.now();
18
19
  let dbOk = false;
19
20
  let dbLatency = 0;
20
21
  try {
21
22
  const t1 = Date.now();
22
- const r = db.prepare('SELECT 1 as ok').get();
23
+ const r = await dbOne('SELECT 1 as ok');
23
24
  dbLatency = Date.now() - t1;
24
25
  dbOk = r?.ok === 1;
25
26
  }
@@ -37,7 +38,7 @@ export function registerPublicUtilsRoutes(app, deps) {
37
38
  check_ms: Date.now() - t0,
38
39
  });
39
40
  });
40
- app.post('/api/mcp-telemetry', (req, res) => {
41
+ app.post('/api/mcp-telemetry', async (req, res) => {
41
42
  const ip = req.ip || 'unknown';
42
43
  if (!rateLimitOk(ip))
43
44
  return void res.status(429).json({ error: 'rate-limited' });
@@ -55,17 +56,17 @@ export function registerPublicUtilsRoutes(app, deps) {
55
56
  const uih = typeof user_id_hash === 'string' && /^[0-9a-f]{1,32}$/.test(user_id_hash) ? user_id_hash : null;
56
57
  const sv = typeof server_version === 'string' && server_version.length <= 32 ? server_version : null;
57
58
  try {
58
- db.prepare(`
59
+ await dbRun(`
59
60
  INSERT INTO mcp_tool_calls (tool_name, user_id_hash, server_version, outcome, latency_ms)
60
61
  VALUES (?, ?, ?, ?, ?)
61
- `).run(tool_name, uih, sv, outcome, Math.round(lat));
62
+ `, [tool_name, uih, sv, outcome, Math.round(lat)]);
62
63
  }
63
64
  catch { /* swallow — never fail telemetry */ }
64
65
  res.json({ ok: true });
65
66
  });
66
- app.get('/api/system-flags', (_req, res) => {
67
- const requireRef = db.prepare("SELECT value FROM system_state WHERE key='require_ref_to_register'").get()?.value === '1';
68
- const inviteRotation = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
67
+ app.get('/api/system-flags', async (_req, res) => {
68
+ const requireRef = (await dbOne("SELECT value FROM system_state WHERE key='require_ref_to_register'"))?.value === '1';
69
+ const inviteRotation = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
69
70
  // #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
70
71
  const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
71
72
  res.json({
@@ -79,11 +80,11 @@ export function registerPublicUtilsRoutes(app, deps) {
79
80
  // /api/protocol-status — JSON API 别名(同份内容)
80
81
  // 内容:network_state(协议处于哪一阶段 + 诚实免责) + issuers(信任锚地址 + 轮换历史)
81
82
  // 信任锚是 Phase 4 凭证(/api/me/agents/:prefix/passport)的验签依据,陌生第三方靠这个端点找到"webaz 官方地址"。
82
- function buildProtocolManifest() {
83
- const phase = db.prepare("SELECT value FROM system_state WHERE key='protocol_phase'").get()?.value || 'pre_launch';
83
+ async function buildProtocolManifest() {
84
+ const phase = (await dbOne("SELECT value FROM system_state WHERE key='protocol_phase'"))?.value || 'pre_launch';
84
85
  // real_users = 已绑 Passkey 的账号数(我们的"真人"定义);pre-launch 应当 ≈0
85
- const realUsers = db.prepare("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials").get()?.n ?? 0;
86
- const issuerActiveSince = db.prepare("SELECT value FROM system_state WHERE key='issuer_active_since'").get()?.value || '2026-05-30';
86
+ const realUsers = (await dbOne("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials"))?.n ?? 0;
87
+ const issuerActiveSince = (await dbOne("SELECT value FROM system_state WHERE key='issuer_active_since'"))?.value || '2026-05-30';
87
88
  return {
88
89
  name: 'WebAZ Protocol',
89
90
  // RFC-011 §④ 两轴版本(单一来源 src/version.ts):
@@ -152,11 +153,12 @@ export function registerPublicUtilsRoutes(app, deps) {
152
153
  change_feed: 'https://webaz.xyz/api/agent/changes', // ④ 契约变更 + 指纹 + 弃用
153
154
  verifiability_index: 'https://webaz.xyz/.well-known/webaz-verifiability.json', // ⑤ 什么可验+怎么验
154
155
  economic_participation: 'https://webaz.xyz/.well-known/webaz-economic.json', // ⑧ value-participant 角色经济条款(费率实时)
156
+ launch_pulse: 'https://webaz.xyz/.well-known/webaz-launch-pulse.json', // L2 follow-the-launch:诚实 live 计数 + 动量 + 里程碑
155
157
  negative_space: 'https://webaz.xyz/.well-known/webaz-negative-space.json', // ② 负空间(禁区 + 限额 + 后果阶梯)
156
158
  event_stream: 'https://webaz.xyz/api/agent/events?since=<cursor>', // ⑥ 事件游标流(party-gated,需 auth)
157
159
  passport: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport', // ⑤ 可验护照
158
160
  did: 'https://webaz.xyz/.well-known/did.json',
159
- acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP 风格商品发现 feed(只读;is_eligible_checkout=false)
161
+ acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP-inspired 商品【发现】投影(非 strict ACP-ingestable;只读;is_eligible_checkout=false;见 feed.compatibility)
160
162
  },
161
163
  // 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
162
164
  roadmap: {
@@ -183,13 +185,66 @@ export function registerPublicUtilsRoutes(app, deps) {
183
185
  },
184
186
  };
185
187
  }
186
- app.get('/.well-known/webaz-protocol.json', (_req, res) => {
188
+ app.get('/.well-known/webaz-protocol.json', async (_req, res) => {
187
189
  res.setHeader('Cache-Control', 'public, max-age=300'); // 5min 边缘缓存,降轮询
188
- res.json(buildProtocolManifest());
190
+ res.json(await buildProtocolManifest());
189
191
  });
190
- app.get('/api/protocol-status', (_req, res) => {
192
+ app.get('/api/protocol-status', async (_req, res) => {
191
193
  res.setHeader('Cache-Control', 'public, max-age=300');
192
- res.json(buildProtocolManifest());
194
+ res.json(await buildProtocolManifest());
195
+ });
196
+ // L2 onboarding「follow the launch」(2026-06-08):pre-launch 没有"买"作为回访理由,
197
+ // 给早期信徒一个【诚实】的"看协议苏醒"面 —— 真实计数 + 7d 动量 + 里程碑(firsts),零粉饰。
198
+ // 纯公开读(无 auth),网站侧(不进 MCP 包,Railway 部署即生效)。
199
+ // RFC-016 Phase 0 试点:本函数改用异步 DB seam(dbOne)。其余 call site 后续分批迁。
200
+ async function buildLaunchPulse() {
201
+ const count = async (sql) => ((await dbOne(sql))?.n ?? 0);
202
+ const firstAt = async (sql) => ((await dbOne(sql))?.t ?? null);
203
+ const phase = (await dbOne("SELECT value FROM system_state WHERE key='protocol_phase'"))?.value || 'pre_launch';
204
+ const [passkey, sellers, products, completed, disputesResolved, newPasskey7d, orders7d, firstSeller, firstProduct, firstOrder, firstCompleted, firstDispute] = await Promise.all([
205
+ count("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials"),
206
+ count("SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%seller%' AND (deleted_at IS NULL OR deleted_at = '')"),
207
+ count("SELECT COUNT(*) AS n FROM products WHERE status='active'"),
208
+ count("SELECT COUNT(*) AS n FROM orders WHERE status='completed'"),
209
+ count("SELECT COUNT(*) AS n FROM disputes WHERE status='resolved'"),
210
+ count("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials WHERE created_at > datetime('now','-7 day')"),
211
+ count("SELECT COUNT(*) AS n FROM orders WHERE created_at > datetime('now','-7 day')"),
212
+ firstAt("SELECT MIN(created_at) AS t FROM users WHERE roles LIKE '%seller%'"),
213
+ firstAt("SELECT MIN(created_at) AS t FROM products"),
214
+ firstAt("SELECT MIN(created_at) AS t FROM orders"),
215
+ firstAt("SELECT MIN(updated_at) AS t FROM orders WHERE status='completed'"),
216
+ firstAt("SELECT MIN(resolved_at) AS t FROM disputes WHERE status='resolved'"),
217
+ ]);
218
+ return {
219
+ phase,
220
+ as_of: new Date().toISOString(),
221
+ note: 'Honest pre-launch pulse — real counts, zero inflation. Watch the protocol wake up; come back to see it grow.',
222
+ participants: { passkey_bound_humans: passkey, sellers },
223
+ catalog: { active_products: products },
224
+ activity: {
225
+ completed_orders: completed,
226
+ disputes_resolved: disputesResolved,
227
+ new_passkey_humans_7d: newPasskey7d,
228
+ orders_7d: orders7d,
229
+ },
230
+ milestones: {
231
+ first_seller_at: firstSeller,
232
+ first_product_listed_at: firstProduct,
233
+ first_order_at: firstOrder,
234
+ first_order_completed_at: firstCompleted,
235
+ first_dispute_resolved_at: firstDispute,
236
+ },
237
+ next: 'Public launch unlocks real settlement. Follow it / get notified at launch + request an invite: https://webaz.xyz/#welcome',
238
+ honesty: 'Pre-launch: WAZ is a simulated test currency; no real money settles yet. These numbers are the live protocol state, not market-size or investment signal.',
239
+ };
240
+ }
241
+ app.get('/.well-known/webaz-launch-pulse.json', async (_req, res) => {
242
+ res.setHeader('Cache-Control', 'public, max-age=120');
243
+ res.json(await buildLaunchPulse());
244
+ });
245
+ app.get('/api/launch-pulse', async (_req, res) => {
246
+ res.setHeader('Cache-Control', 'public, max-age=120');
247
+ res.json(await buildLaunchPulse());
193
248
  });
194
249
  // RFC-011 §② — agent 可读能力矩阵(写边界 action-scope + 敏感读 scope)。
195
250
  // live = 直接序列化 enforce 用的规则表(src/pwa/endpoint-actions.ts),doc=code 零漂移。
@@ -266,32 +321,39 @@ export function registerPublicUtilsRoutes(app, deps) {
266
321
  });
267
322
  // RFC-011 §⑧ 经济参与索引 —— value-participant 角色 × 赚什么/押什么/担什么责,
268
323
  // 费率【实时】从 protocol_params 读(doc=code,永不和 enforced 经济漂移)。
269
- const liveParam = (key, fallback) => {
270
- const row = db.prepare('SELECT value, type FROM protocol_params WHERE key = ?').get(key);
271
- if (!row)
272
- return fallback;
273
- if (row.type === 'number')
274
- return Number(row.value);
275
- if (row.type === 'boolean')
276
- return (row.value === 'true' || row.value === '1');
277
- return row.value;
324
+ // RFC-016: 一次性异步预取全部 protocol_params Map,再返回同步 getter,喂给
325
+ // buildEconomicParticipation / buildNegativeSpace(保持其同步签名,不动共享 module),仍是 doc=code 实时读。
326
+ const loadLiveParam = async () => {
327
+ const paramRows = await dbAll('SELECT key, value, type FROM protocol_params');
328
+ const paramMap = new Map(paramRows.map(r => [r.key, r]));
329
+ return (key, fallback) => {
330
+ const row = paramMap.get(key);
331
+ if (!row)
332
+ return fallback;
333
+ if (row.type === 'number')
334
+ return Number(row.value);
335
+ if (row.type === 'boolean')
336
+ return (row.value === 'true' || row.value === '1');
337
+ return row.value;
338
+ };
278
339
  };
279
- const economic = (_req, res) => {
340
+ const economic = async (_req, res) => {
280
341
  res.setHeader('Cache-Control', 'public, max-age=300');
281
- res.json(buildEconomicParticipation(liveParam));
342
+ res.json(buildEconomicParticipation(await loadLiveParam()));
282
343
  };
283
344
  app.get('/.well-known/webaz-economic.json', economic);
284
345
  app.get('/api/agent/economic-participation', economic);
285
346
  // RFC-011 §② 负空间 —— 禁区 + enforced 限额 + 后果阶梯(per-agent 速率实时读)。
286
- const negativeSpace = (_req, res) => {
347
+ const negativeSpace = async (_req, res) => {
287
348
  res.setHeader('Cache-Control', 'public, max-age=300');
288
- res.json(buildNegativeSpace(liveParam));
349
+ res.json(buildNegativeSpace(await loadLiveParam()));
289
350
  };
290
351
  app.get('/.well-known/webaz-negative-space.json', negativeSpace);
291
352
  app.get('/api/agent/negative-space', negativeSpace);
292
- // RFC-015 P0 —— ACP product feed:把现有商品投影成 OpenAI Agentic Commerce 的 feed 形状,
293
- // 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)。诚实门控:is_eligible_checkout false
294
- // (ACP /complete 是卡+PSP,WebAZ 未接);currency=WAZ 是模拟单位。详见 buildAcpProductFeed + RFC-015。
353
+ // RFC-015 P0 —— ACP-inspired 商品【发现】投影:把现有商品投影成 OpenAI Agentic Commerce 的 feed 形状,
354
+ // 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)。【非 strict ACP-ingestable feed】(Codex #151):
355
+ // currency=WAZ 非 ISO 4217、is_eligible_checkout 恒 false(ACP /complete 是卡+PSP,WebAZ 未接)
356
+ // 不发 target_countries/store_country 等商家必填字段 —— 非合规点逐条见 feed.compatibility。详见 buildAcpProductFeed + RFC-015。
295
357
  const acpFeed = (_req, res) => {
296
358
  res.setHeader('Cache-Control', 'public, max-age=300');
297
359
  res.json(buildAcpProductFeed(db));
@@ -338,8 +400,8 @@ export function registerPublicUtilsRoutes(app, deps) {
338
400
  ],
339
401
  });
340
402
  });
341
- app.get('/api/editor-picks', (_req, res) => {
342
- const products = db.prepare(`
403
+ app.get('/api/editor-picks', async (_req, res) => {
404
+ const products = await dbAll(`
343
405
  SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
344
406
  p.title as product_title, p.price, p.images, p.category,
345
407
  u.handle as seller_handle
@@ -348,30 +410,30 @@ export function registerPublicUtilsRoutes(app, deps) {
348
410
  JOIN users u ON u.id = p.seller_id
349
411
  WHERE ep.kind = 'product' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
350
412
  ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
351
- `).all();
352
- const sellers = db.prepare(`
413
+ `);
414
+ const sellers = await dbAll(`
353
415
  SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
354
416
  u.handle, u.name, u.shop_banner_url, u.bio
355
417
  FROM editor_picks ep
356
418
  JOIN users u ON u.id = ep.target_id AND u.role = 'seller'
357
419
  WHERE ep.kind = 'seller' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
358
420
  ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
359
- `).all();
421
+ `);
360
422
  res.json({ products, sellers });
361
423
  });
362
- app.get('/api/manifest', (_req, res) => {
363
- res.json(generateManifest(db));
424
+ app.get('/api/manifest', async (_req, res) => {
425
+ res.json(await generateManifest(db));
364
426
  });
365
427
  // W3.5-B 治理上岗公开 stats(docs/GOVERNANCE-ONBOARDING.md)
366
428
  // 无 auth — agent / 用户 / 第三方都可读;不暴露 PII
367
- app.get('/api/governance/onboarding-stats', (_req, res) => {
429
+ app.get('/api/governance/onboarding-stats', async (_req, res) => {
368
430
  res.setHeader('Cache-Control', 'public, max-age=300');
369
431
  try {
370
432
  // active counts(users.roles 含 arbitrator / verifier 的人数,fixture 也算)
371
- const arbitratorCount = db.prepare(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%arbitrator%' AND (deleted_at IS NULL OR deleted_at = '')`).get()?.n ?? 0;
372
- const verifierCount = db.prepare(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%verifier%' AND (deleted_at IS NULL OR deleted_at = '')`).get()?.n ?? 0;
433
+ const arbitratorCount = (await dbOne(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%arbitrator%' AND (deleted_at IS NULL OR deleted_at = '')`))?.n ?? 0;
434
+ const verifierCount = (await dbOne(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%verifier%' AND (deleted_at IS NULL OR deleted_at = '')`))?.n ?? 0;
373
435
  // pending applications
374
- const pendingCount = db.prepare(`SELECT COUNT(*) AS n FROM governance_applications WHERE status = 'pending_onboarding'`).get()?.n ?? 0;
436
+ const pendingCount = (await dbOne(`SELECT COUNT(*) AS n FROM governance_applications WHERE status = 'pending_onboarding'`))?.n ?? 0;
375
437
  // 资格门槛 snapshot(给前端 pre-check 显示)
376
438
  // ⚠️ 2026-06-03 #4 修:此前这里 dump 装饰性 protocol_params.governance_onboarding.*,
377
439
  // 与代码实际 enforce 的门槛不符(例 min_completed_orders param=5,但代码 arbitrator 要 50 /
@@ -380,10 +442,10 @@ export function registerPublicUtilsRoutes(app, deps) {
380
442
  // ⚠️ 必须与 server.ts checkArbitratorEligibility / checkVerifierEligibility 保持同步。
381
443
  const eligibility = {
382
444
  arbitrator: { registration_days: 90, completed_orders: 50, reputation: 300, balance_waz: 500, email_verified: true, zero_disputes_lost: true, never_suspended: true },
383
- verifier: { registration_days: 60, completed_orders: 20, email_verified: true, zero_disputes_lost: true, never_suspended: true },
445
+ verifier: { registration_days: 60, completed_orders: 20, reputation: 110, balance_waz: 200, email_verified: true, zero_disputes_lost: true, never_suspended: true },
384
446
  };
385
447
  // quiz_pass_score 是真正被代码读取的 param(governance-onboarding.ts quiz-submit),保留。
386
- const quizPassRow = db.prepare(`SELECT value FROM protocol_params WHERE key = 'governance_onboarding.quiz_pass_score'`).get();
448
+ const quizPassRow = await dbOne(`SELECT value FROM protocol_params WHERE key = 'governance_onboarding.quiz_pass_score'`);
387
449
  const quizPassScore = Number(quizPassRow?.value ?? 80);
388
450
  res.json({
389
451
  phase: 'A',
@@ -1,17 +1,20 @@
1
- /** web-push 失败回调:删除已失效订阅。导出以备 P1-5 任务调用。 */
2
- export function cleanupStaleSubscription(db, endpoint) {
1
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ /** web-push 失败回调:删除已失效订阅。导出以备 P1-5 任务调用。
3
+ * RFC-016: db 参数保留(调用方签名兼容),内部走异步 seam(同实例,setSeamDb)。 */
4
+ export async function cleanupStaleSubscription(_db, endpoint) {
3
5
  if (!endpoint)
4
6
  return;
5
- db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint);
7
+ await dbRun('DELETE FROM push_subscriptions WHERE endpoint = ?', [endpoint]);
6
8
  }
7
9
  export function registerPushRoutes(app, deps) {
8
- const { db, generateId, auth, vapidPublicKey } = deps;
10
+ // db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
11
+ const { generateId, auth, vapidPublicKey } = deps;
9
12
  app.get('/api/push/vapid-public-key', (_req, res) => {
10
13
  if (!vapidPublicKey)
11
14
  return void res.status(503).json({ error: '推送未配置,请联系管理员设置 VAPID_PUBLIC_KEY' });
12
15
  res.json({ key: vapidPublicKey });
13
16
  });
14
- app.post('/api/push/subscribe', (req, res) => {
17
+ app.post('/api/push/subscribe', async (req, res) => {
15
18
  const user = auth(req, res);
16
19
  if (!user)
17
20
  return;
@@ -21,34 +24,32 @@ export function registerPushRoutes(app, deps) {
21
24
  }
22
25
  const id = generateId('psub');
23
26
  // 同 user + endpoint 视作重新订阅
24
- const existing = db.prepare('SELECT id FROM push_subscriptions WHERE user_id = ? AND endpoint = ?').get(user.id, String(endpoint));
27
+ const existing = await dbOne('SELECT id FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
25
28
  if (existing) {
26
- db.prepare('UPDATE push_subscriptions SET p256dh = ?, auth = ?, user_agent = ?, enabled = 1 WHERE id = ?')
27
- .run(String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null, existing.id);
29
+ await dbRun('UPDATE push_subscriptions SET p256dh = ?, auth = ?, user_agent = ?, enabled = 1 WHERE id = ?', [String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null, existing.id]);
28
30
  return void res.json({ success: true, id: existing.id });
29
31
  }
30
- db.prepare(`INSERT INTO push_subscriptions (id, user_id, endpoint, p256dh, auth, user_agent) VALUES (?,?,?,?,?,?)`)
31
- .run(id, user.id, String(endpoint), String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null);
32
+ await dbRun(`INSERT INTO push_subscriptions (id, user_id, endpoint, p256dh, auth, user_agent) VALUES (?,?,?,?,?,?)`, [id, user.id, String(endpoint), String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null]);
32
33
  res.json({ success: true, id });
33
34
  });
34
- app.delete('/api/push/subscribe', (req, res) => {
35
+ app.delete('/api/push/subscribe', async (req, res) => {
35
36
  const user = auth(req, res);
36
37
  if (!user)
37
38
  return;
38
39
  const { endpoint } = req.body || {};
39
40
  if (endpoint) {
40
- db.prepare('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?').run(user.id, String(endpoint));
41
+ await dbRun('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
41
42
  }
42
43
  else {
43
- db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(user.id);
44
+ await dbRun('DELETE FROM push_subscriptions WHERE user_id = ?', [user.id]);
44
45
  }
45
46
  res.json({ success: true });
46
47
  });
47
- app.get('/api/push/status', (req, res) => {
48
+ app.get('/api/push/status', async (req, res) => {
48
49
  const user = auth(req, res);
49
50
  if (!user)
50
51
  return;
51
- const cnt = db.prepare('SELECT COUNT(*) as n FROM push_subscriptions WHERE user_id = ? AND enabled = 1').get(user.id).n;
52
+ const cnt = (await dbOne('SELECT COUNT(*) as n FROM push_subscriptions WHERE user_id = ? AND enabled = 1', [user.id])).n;
52
53
  res.json({ subscribed: cnt > 0, count: cnt, vapid_configured: !!vapidPublicKey });
53
54
  });
54
55
  }