@seasonkoh/webaz 0.1.24 → 0.1.26

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 (195) hide show
  1. package/README.md +5 -1
  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 +288 -208
  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 +182 -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 +11 -3
  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-discovery.js +55 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  38. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  39. package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
  40. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
  41. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  42. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  43. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  44. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  45. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  46. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  47. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  48. package/dist/pwa/acp-feed.js +13 -1
  49. package/dist/pwa/admin-bearer-auth.js +21 -0
  50. package/dist/pwa/contract-fingerprint.js +2 -0
  51. package/dist/pwa/email-delivery.js +127 -0
  52. package/dist/pwa/endpoint-actions.js +5 -1
  53. package/dist/pwa/goal-index.js +8 -8
  54. package/dist/pwa/human-presence.js +62 -0
  55. package/dist/pwa/public/app.js +1485 -283
  56. package/dist/pwa/public/i18n.js +297 -59
  57. package/dist/pwa/public/index.html +1 -0
  58. package/dist/pwa/public/openapi.json +5 -5
  59. package/dist/pwa/public/whitepaper/en/index.html +153 -0
  60. package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
  61. package/dist/pwa/rate-limit.js +22 -0
  62. package/dist/pwa/routes/account-deletion.js +15 -13
  63. package/dist/pwa/routes/addresses.js +10 -9
  64. package/dist/pwa/routes/admin-admins.js +13 -14
  65. package/dist/pwa/routes/admin-analytics.js +109 -69
  66. package/dist/pwa/routes/admin-atomic.js +10 -4
  67. package/dist/pwa/routes/admin-catalog.js +13 -11
  68. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  69. package/dist/pwa/routes/admin-events.js +5 -3
  70. package/dist/pwa/routes/admin-health.js +2 -1
  71. package/dist/pwa/routes/admin-moderation.js +50 -29
  72. package/dist/pwa/routes/admin-ops.js +35 -23
  73. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  74. package/dist/pwa/routes/admin-reports.js +23 -21
  75. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  76. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  77. package/dist/pwa/routes/admin-users-query.js +65 -53
  78. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  79. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  80. package/dist/pwa/routes/admin-wallet-ops.js +32 -7
  81. package/dist/pwa/routes/agent-buy.js +46 -22
  82. package/dist/pwa/routes/agent-governance.js +52 -56
  83. package/dist/pwa/routes/ai.js +7 -5
  84. package/dist/pwa/routes/analytics.js +43 -41
  85. package/dist/pwa/routes/anchors.js +19 -20
  86. package/dist/pwa/routes/announcements.js +13 -13
  87. package/dist/pwa/routes/arbitrator.js +97 -31
  88. package/dist/pwa/routes/auction.js +157 -116
  89. package/dist/pwa/routes/auth-login.js +6 -4
  90. package/dist/pwa/routes/auth-read.js +21 -10
  91. package/dist/pwa/routes/auth-register.js +111 -26
  92. package/dist/pwa/routes/auth-sessions.js +12 -11
  93. package/dist/pwa/routes/blocklist.js +16 -15
  94. package/dist/pwa/routes/build-feedback.js +10 -9
  95. package/dist/pwa/routes/build-reputation.js +6 -2
  96. package/dist/pwa/routes/build-tasks.js +45 -13
  97. package/dist/pwa/routes/buyer-feeds.js +27 -25
  98. package/dist/pwa/routes/cart.js +16 -15
  99. package/dist/pwa/routes/charity.js +212 -150
  100. package/dist/pwa/routes/chat.js +42 -43
  101. package/dist/pwa/routes/checkin-tasks.js +10 -9
  102. package/dist/pwa/routes/checkout-helpers.js +12 -10
  103. package/dist/pwa/routes/claim-initiators.js +34 -14
  104. package/dist/pwa/routes/claim-verify.js +86 -53
  105. package/dist/pwa/routes/claim-voting.js +43 -18
  106. package/dist/pwa/routes/contribution-identity.js +164 -0
  107. package/dist/pwa/routes/contribution-score.js +19 -0
  108. package/dist/pwa/routes/coupons.js +19 -16
  109. package/dist/pwa/routes/dashboards.js +18 -16
  110. package/dist/pwa/routes/dispute-cases.js +25 -24
  111. package/dist/pwa/routes/disputes-read.js +45 -51
  112. package/dist/pwa/routes/disputes-write.js +124 -61
  113. package/dist/pwa/routes/evidence.js +9 -9
  114. package/dist/pwa/routes/external-anchors.js +13 -12
  115. package/dist/pwa/routes/feedback.js +29 -33
  116. package/dist/pwa/routes/flash-sales.js +18 -16
  117. package/dist/pwa/routes/follows.js +25 -24
  118. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  119. package/dist/pwa/routes/governance-onboarding.js +70 -59
  120. package/dist/pwa/routes/group-buys.js +22 -22
  121. package/dist/pwa/routes/growth.js +34 -31
  122. package/dist/pwa/routes/import-product.js +12 -10
  123. package/dist/pwa/routes/kyc.js +9 -8
  124. package/dist/pwa/routes/leaderboard.js +20 -18
  125. package/dist/pwa/routes/listings.js +23 -22
  126. package/dist/pwa/routes/logistics.js +10 -8
  127. package/dist/pwa/routes/manifests.js +27 -27
  128. package/dist/pwa/routes/me-data.js +23 -21
  129. package/dist/pwa/routes/notifications.js +7 -6
  130. package/dist/pwa/routes/offers.js +30 -12
  131. package/dist/pwa/routes/orders-action.js +51 -29
  132. package/dist/pwa/routes/orders-create.js +75 -20
  133. package/dist/pwa/routes/orders-read.js +21 -20
  134. package/dist/pwa/routes/p2p-products.js +30 -18
  135. package/dist/pwa/routes/payments-governance.js +61 -56
  136. package/dist/pwa/routes/peers.js +9 -8
  137. package/dist/pwa/routes/pin-receipts.js +13 -13
  138. package/dist/pwa/routes/products-aliases.js +12 -10
  139. package/dist/pwa/routes/products-claims.js +36 -17
  140. package/dist/pwa/routes/products-create.js +53 -38
  141. package/dist/pwa/routes/products-crud.js +17 -16
  142. package/dist/pwa/routes/products-links.js +49 -26
  143. package/dist/pwa/routes/products-list.js +6 -4
  144. package/dist/pwa/routes/products-meta.js +40 -39
  145. package/dist/pwa/routes/products-update.js +19 -5
  146. package/dist/pwa/routes/profile-credentials.js +20 -19
  147. package/dist/pwa/routes/profile-identity.js +14 -13
  148. package/dist/pwa/routes/profile-location.js +7 -6
  149. package/dist/pwa/routes/profile-placement.js +20 -19
  150. package/dist/pwa/routes/profile-prefs.js +11 -11
  151. package/dist/pwa/routes/promoter.js +58 -66
  152. package/dist/pwa/routes/public-build-tasks.js +19 -0
  153. package/dist/pwa/routes/public-utils.js +108 -46
  154. package/dist/pwa/routes/push.js +16 -15
  155. package/dist/pwa/routes/ratings.js +92 -32
  156. package/dist/pwa/routes/recover-key.js +66 -26
  157. package/dist/pwa/routes/referral.js +37 -52
  158. package/dist/pwa/routes/reputation.js +3 -2
  159. package/dist/pwa/routes/returns.js +76 -73
  160. package/dist/pwa/routes/reviews.js +41 -18
  161. package/dist/pwa/routes/rewards-apply.js +16 -15
  162. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  163. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  164. package/dist/pwa/routes/rfqs.js +163 -85
  165. package/dist/pwa/routes/search.js +16 -14
  166. package/dist/pwa/routes/secondhand.js +25 -22
  167. package/dist/pwa/routes/seller-quota.js +24 -26
  168. package/dist/pwa/routes/share-redirects.js +60 -55
  169. package/dist/pwa/routes/shareables-interactions.js +34 -35
  170. package/dist/pwa/routes/shareables.js +55 -51
  171. package/dist/pwa/routes/shop-referral.js +58 -0
  172. package/dist/pwa/routes/shops.js +25 -20
  173. package/dist/pwa/routes/signaling.js +10 -9
  174. package/dist/pwa/routes/skill-market.js +16 -16
  175. package/dist/pwa/routes/skills.js +15 -14
  176. package/dist/pwa/routes/snf.js +14 -13
  177. package/dist/pwa/routes/tags.js +10 -9
  178. package/dist/pwa/routes/task-proposals.js +121 -0
  179. package/dist/pwa/routes/trial.js +72 -52
  180. package/dist/pwa/routes/trusted-kpi.js +20 -18
  181. package/dist/pwa/routes/url-claim.js +67 -28
  182. package/dist/pwa/routes/users-public.js +62 -70
  183. package/dist/pwa/routes/variants.js +12 -13
  184. package/dist/pwa/routes/verifier-user.js +61 -21
  185. package/dist/pwa/routes/verify-tasks.js +49 -25
  186. package/dist/pwa/routes/waitlist.js +16 -15
  187. package/dist/pwa/routes/wallet-read.js +75 -37
  188. package/dist/pwa/routes/wallet-write.js +12 -9
  189. package/dist/pwa/routes/webauthn.js +25 -26
  190. package/dist/pwa/routes/webhooks.js +26 -26
  191. package/dist/pwa/routes/welcome.js +45 -50
  192. package/dist/pwa/routes/wishlist-qa.js +29 -32
  193. package/dist/pwa/server.js +304 -90
  194. package/dist/version.js +1 -1
  195. package/package.json +76 -3
@@ -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,
@@ -88,18 +90,9 @@ export function registerPromoterRoutes(app, deps) {
88
90
  next_30_estimate: earnedLast30 + wazLast30,
89
91
  };
90
92
  const insights = [];
91
- const leftPv = Number(myUser?.total_left_pv ?? 0);
92
- const rightPv = Number(myUser?.total_right_pv ?? 0);
93
- if (leftPv > 0 || rightPv > 0) {
94
- const max = Math.max(leftPv, rightPv), min = Math.min(leftPv, rightPv);
95
- const ratio = max > 0 ? min / max : 0;
96
- const weak = leftPv < rightPv ? '左区' : '右区';
97
- if (ratio < 0.5)
98
- insights.push({ type: 'weak_leg', level: 'warn', text: `${weak} PV 仅为强腿的 ${(ratio * 100).toFixed(0)}% — 建议主推${weak},对碰 = min(L,R) × tier_score` });
99
- else
100
- insights.push({ type: 'balanced', level: 'success', text: `双腿均衡度 ${(ratio * 100).toFixed(0)}% — 对碰节奏健康` });
101
- }
102
- const lastInvite = db.prepare(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`).get(userId);
93
+ // pre-public de-MLM:移除弱腿 / pairing / PV-tier 经营建议 —— PV 对碰为 pre-launch、未对用户启用,
94
+ // 不在用户面 surface 营销主推弱腿 / pairing 公式 / PV-tier 进度等玩法。位置 / PV 仅为参与记录,非收益路径。
95
+ const lastInvite = (await dbOne(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`, [userId]));
103
96
  if (lastInvite.t) {
104
97
  const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
105
98
  if (days > 14)
@@ -110,21 +103,16 @@ export function registerPromoterRoutes(app, deps) {
110
103
  else if (l1 === 0) {
111
104
  insights.push({ type: 'no_team', level: 'info', text: `还没有直推 — 先分享你买过且好评的商品给好友` });
112
105
  }
113
- const pair = Math.min(leftPv, rightPv);
114
- const nextTier = tiers.find(t => t.pv_threshold > pair);
115
- if (nextTier && pair > 0 && pair / nextTier.pv_threshold > 0.7) {
116
- insights.push({ type: 'near_tier', level: 'success', text: `距离 tier ${nextTier.tier} 仅差 ${(nextTier.pv_threshold - pair).toLocaleString()} PV (+${nextTier.score_per_hit} Score / 次)` });
117
- }
118
106
  if (!canL1Share && completedOrders === 0) {
119
- insights.push({ type: 'unlock', level: 'info', text: `完成首笔购买可解锁分享奖励 当前仅可分享 PV 加人扩双轨树` });
107
+ insights.push({ type: 'share_hint', level: 'info', text: `完成首笔购买后可使用分享功能;分享记录仅作归因 / 参与记录,不构成收益承诺。` });
120
108
  }
121
109
  if (shareableProducts.length > 0 && grand === 0) {
122
110
  insights.push({ type: 'first_share', level: 'info', text: `你有 ${shareableProducts.length} 个可分享商品但暂无成交 — 试着把链接发给身边的人` });
123
111
  }
124
- const treeNode = (uid) => {
112
+ const treeNode = async (uid) => {
125
113
  if (!uid)
126
114
  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);
115
+ 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
116
  if (!u)
129
117
  return null;
130
118
  return {
@@ -135,25 +123,29 @@ export function registerPromoterRoutes(app, deps) {
135
123
  right_id: u.right_child_id ?? null,
136
124
  };
137
125
  };
138
- const me_node = treeNode(userId);
139
- const left_node = treeNode(myUser?.left_child_id);
140
- const right_node = treeNode(myUser?.right_child_id);
126
+ const me_node = await treeNode(userId);
127
+ const left_node = await treeNode(myUser?.left_child_id);
128
+ const right_node = await treeNode(myUser?.right_child_id);
141
129
  const binaryTree = {
142
130
  me: me_node,
143
131
  left: left_node,
144
132
  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),
133
+ ll: await treeNode(left_node?.left_id),
134
+ lr: await treeNode(left_node?.right_id),
135
+ rl: await treeNode(right_node?.left_id),
136
+ rr: await treeNode(right_node?.right_id),
149
137
  };
150
- const meCard = db.prepare("SELECT permanent_code, handle FROM users WHERE id = ?").get(userId);
151
- const codeForLink = meCard?.permanent_code || userId;
138
+ const meCard = await dbOne("SELECT permanent_code, handle FROM users WHERE id = ?", [userId]);
139
+ // invite links use permanent_code ONLY — never fall back to user_id (would leak usr_xxx into ?ref).
140
+ const codeForLink = meCard?.permanent_code || null;
141
+ const host = `${req.protocol}://${req.get('host')}`;
152
142
  res.json({
153
143
  user_id: userId,
154
144
  permanent_code: meCard?.permanent_code || null,
155
145
  handle: meCard?.handle || null,
156
- referral_link: `${req.protocol}://${req.get('host')}/i/${codeForLink}`,
146
+ invite_code_available: !!codeForLink,
147
+ referral_link: codeForLink ? `${host}/i/${codeForLink}` : null,
148
+ invite_unavailable_reason: codeForLink ? null : 'permanent_code_missing — refresh or contact support',
157
149
  region: me?.region || 'global',
158
150
  my_sponsor: mySponsor ? { id: me.sponsor_id, name: mySponsor.name } : null,
159
151
  permissions: {
@@ -176,8 +168,8 @@ export function registerPromoterRoutes(app, deps) {
176
168
  projection,
177
169
  insights,
178
170
  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`,
171
+ left_invite_url: codeForLink ? `${host}/i/${codeForLink}-L` : null,
172
+ right_invite_url: codeForLink ? `${host}/i/${codeForLink}-R` : null,
181
173
  total_left_pv: Number(myUser?.total_left_pv ?? 0),
182
174
  total_right_pv: Number(myUser?.total_right_pv ?? 0),
183
175
  left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
@@ -191,18 +183,18 @@ export function registerPromoterRoutes(app, deps) {
191
183
  });
192
184
  });
193
185
  // 直推 L1 列表
194
- app.get('/api/promoter/team', (req, res) => {
186
+ app.get('/api/promoter/team', async (req, res) => {
195
187
  const user = auth(req, res);
196
188
  if (!user)
197
189
  return;
198
190
  const userId = user.id;
199
- const rows = db.prepare(`
191
+ const rows = await dbAll(`
200
192
  SELECT u.id, u.name, u.created_at, u.region,
201
193
  (SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as their_l1,
202
194
  COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = ? AND source_buyer_id = u.id), 0) as my_earned_from_them
203
195
  FROM users u WHERE u.sponsor_id = ?
204
196
  ORDER BY u.created_at DESC LIMIT 100
205
- `).all(userId, userId);
197
+ `, [userId, userId]);
206
198
  res.json({ team: rows });
207
199
  });
208
200
  }
@@ -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
  }