@seasonkoh/webaz 0.1.8 → 0.1.10

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 (154) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3679 -852
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31362 -2320
  22. package/dist/pwa/public/i18n.js +5308 -111
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +4 -1
  25. package/dist/pwa/public/manifest.json +39 -4
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +278 -5
  28. package/dist/pwa/public/sw.js +41 -2
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +386 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/ap2-mandate.js +121 -0
  58. package/dist/pwa/routes/arbitrator.js +117 -0
  59. package/dist/pwa/routes/auction.js +436 -0
  60. package/dist/pwa/routes/auth-login.js +40 -0
  61. package/dist/pwa/routes/auth-read.js +66 -0
  62. package/dist/pwa/routes/auth-register.js +161 -0
  63. package/dist/pwa/routes/auth-sessions.js +62 -0
  64. package/dist/pwa/routes/blocklist.js +60 -0
  65. package/dist/pwa/routes/buyer-feeds.js +224 -0
  66. package/dist/pwa/routes/cart.js +155 -0
  67. package/dist/pwa/routes/charity.js +816 -0
  68. package/dist/pwa/routes/chat.js +318 -0
  69. package/dist/pwa/routes/checkin-tasks.js +122 -0
  70. package/dist/pwa/routes/checkout-helpers.js +106 -0
  71. package/dist/pwa/routes/claim-initiators.js +88 -0
  72. package/dist/pwa/routes/claim-verify.js +615 -0
  73. package/dist/pwa/routes/claim-voting.js +114 -0
  74. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  75. package/dist/pwa/routes/coupons.js +165 -0
  76. package/dist/pwa/routes/dashboards.js +99 -0
  77. package/dist/pwa/routes/dispute-cases.js +267 -0
  78. package/dist/pwa/routes/disputes-read.js +358 -0
  79. package/dist/pwa/routes/disputes-write.js +475 -0
  80. package/dist/pwa/routes/evidence.js +86 -0
  81. package/dist/pwa/routes/external-anchors.js +107 -0
  82. package/dist/pwa/routes/feedback.js +270 -0
  83. package/dist/pwa/routes/flash-sales.js +130 -0
  84. package/dist/pwa/routes/follows.js +103 -0
  85. package/dist/pwa/routes/group-buys.js +208 -0
  86. package/dist/pwa/routes/growth.js +199 -0
  87. package/dist/pwa/routes/import-product.js +153 -0
  88. package/dist/pwa/routes/kyc.js +40 -0
  89. package/dist/pwa/routes/leaderboard.js +149 -0
  90. package/dist/pwa/routes/listings.js +281 -0
  91. package/dist/pwa/routes/logistics.js +35 -0
  92. package/dist/pwa/routes/manifests.js +126 -0
  93. package/dist/pwa/routes/me-data.js +101 -0
  94. package/dist/pwa/routes/notifications.js +48 -0
  95. package/dist/pwa/routes/offers.js +96 -0
  96. package/dist/pwa/routes/orders-action.js +285 -0
  97. package/dist/pwa/routes/orders-create.js +382 -0
  98. package/dist/pwa/routes/orders-read.js +180 -0
  99. package/dist/pwa/routes/p2p-products.js +178 -0
  100. package/dist/pwa/routes/payments-governance.js +311 -0
  101. package/dist/pwa/routes/peers.js +34 -0
  102. package/dist/pwa/routes/pin-receipts.js +39 -0
  103. package/dist/pwa/routes/products-aliases.js +119 -0
  104. package/dist/pwa/routes/products-claims.js +60 -0
  105. package/dist/pwa/routes/products-create.js +206 -0
  106. package/dist/pwa/routes/products-crud.js +73 -0
  107. package/dist/pwa/routes/products-links.js +129 -0
  108. package/dist/pwa/routes/products-list.js +424 -0
  109. package/dist/pwa/routes/products-meta.js +155 -0
  110. package/dist/pwa/routes/products-update.js +125 -0
  111. package/dist/pwa/routes/profile-credentials.js +105 -0
  112. package/dist/pwa/routes/profile-identity.js +174 -0
  113. package/dist/pwa/routes/profile-location.js +35 -0
  114. package/dist/pwa/routes/profile-placement.js +70 -0
  115. package/dist/pwa/routes/profile-prefs.js +93 -0
  116. package/dist/pwa/routes/promoter.js +208 -0
  117. package/dist/pwa/routes/public-utils.js +227 -0
  118. package/dist/pwa/routes/push.js +54 -0
  119. package/dist/pwa/routes/ratings.js +220 -0
  120. package/dist/pwa/routes/recover-key.js +100 -0
  121. package/dist/pwa/routes/referral.js +58 -0
  122. package/dist/pwa/routes/reputation.js +34 -0
  123. package/dist/pwa/routes/returns.js +493 -0
  124. package/dist/pwa/routes/reviews.js +81 -0
  125. package/dist/pwa/routes/rfqs.js +443 -0
  126. package/dist/pwa/routes/search.js +172 -0
  127. package/dist/pwa/routes/secondhand.js +278 -0
  128. package/dist/pwa/routes/seller-quota.js +225 -0
  129. package/dist/pwa/routes/share-redirects.js +164 -0
  130. package/dist/pwa/routes/shareables-interactions.js +212 -0
  131. package/dist/pwa/routes/shareables.js +470 -0
  132. package/dist/pwa/routes/shops.js +98 -0
  133. package/dist/pwa/routes/signaling.js +43 -0
  134. package/dist/pwa/routes/skill-market.js +173 -0
  135. package/dist/pwa/routes/skills.js +174 -0
  136. package/dist/pwa/routes/snf.js +126 -0
  137. package/dist/pwa/routes/tags.js +47 -0
  138. package/dist/pwa/routes/trial.js +333 -0
  139. package/dist/pwa/routes/trusted-kpi.js +87 -0
  140. package/dist/pwa/routes/url-claim.js +113 -0
  141. package/dist/pwa/routes/users-public.js +317 -0
  142. package/dist/pwa/routes/variants.js +156 -0
  143. package/dist/pwa/routes/verifier-user.js +107 -0
  144. package/dist/pwa/routes/verify-tasks.js +120 -0
  145. package/dist/pwa/routes/waitlist.js +65 -0
  146. package/dist/pwa/routes/wallet-read.js +218 -0
  147. package/dist/pwa/routes/wallet-write.js +273 -0
  148. package/dist/pwa/routes/webauthn.js +188 -0
  149. package/dist/pwa/routes/webhooks.js +162 -0
  150. package/dist/pwa/routes/welcome.js +226 -0
  151. package/dist/pwa/routes/wishlist-qa.js +135 -0
  152. package/dist/pwa/security/ssrf.js +110 -0
  153. package/dist/pwa/server.js +9317 -2097
  154. package/package.json +8 -3
@@ -0,0 +1,208 @@
1
+ export function registerPromoterRoutes(app, deps) {
2
+ const { db, auth, isAllowedSponsor } = deps;
3
+ app.get('/api/promoter/dashboard', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ 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
+ SELECT COUNT(*) as n FROM users
11
+ WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
12
+ `).get(userId).n;
13
+ const l3 = db.prepare(`
14
+ SELECT COUNT(*) as n FROM users
15
+ WHERE sponsor_id IN (
16
+ SELECT id FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
17
+ )
18
+ `).get(userId).n;
19
+ const earned = db.prepare(`
20
+ SELECT level, COUNT(*) as orders, COALESCE(SUM(amount),0) as total
21
+ FROM commission_records WHERE beneficiary_id = ?
22
+ GROUP BY level
23
+ `).all(userId);
24
+ const byLevel = { 1: { orders: 0, total: 0 }, 2: { orders: 0, total: 0 }, 3: { orders: 0, total: 0 } };
25
+ for (const r of earned)
26
+ byLevel[r.level] = { orders: r.orders, total: r.total };
27
+ const grand = byLevel[1].total + byLevel[2].total + byLevel[3].total;
28
+ const recent = db.prepare(`
29
+ SELECT cr.id, cr.order_id, cr.level, cr.amount, cr.rate, cr.created_at,
30
+ u.name as source_buyer_name
31
+ FROM commission_records cr
32
+ LEFT JOIN users u ON u.id = cr.source_buyer_id
33
+ WHERE cr.beneficiary_id = ?
34
+ 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(`
43
+ SELECT
44
+ COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending_score,
45
+ COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz,
46
+ COUNT(*) as total_hits
47
+ FROM binary_score_records WHERE user_id = ?
48
+ `).get(userId);
49
+ const recentBinary = db.prepare(`
50
+ SELECT id, tier, score, settled_at, waz_amount, created_at
51
+ FROM binary_score_records WHERE user_id = ?
52
+ 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
+ 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(`
59
+ SELECT p.id, p.title, p.price, p.category, p.commission_rate,
60
+ (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as total_sales,
61
+ COALESCE((SELECT SUM(cr.amount) FROM commission_records cr
62
+ JOIN orders o2 ON o2.id = cr.order_id
63
+ WHERE cr.beneficiary_id = ? AND o2.product_id = p.id), 0) as my_earned
64
+ FROM products p
65
+ WHERE p.id IN (SELECT DISTINCT product_id FROM orders WHERE buyer_id = ? AND status = 'completed')
66
+ AND p.commission_rate IS NOT NULL AND p.commission_rate > 0
67
+ AND p.status = 'active'
68
+ ORDER BY my_earned DESC, total_sales DESC LIMIT 20
69
+ `).all(userId, userId);
70
+ const earnedLast30 = db.prepare(`
71
+ SELECT COALESCE(SUM(amount),0) as total FROM commission_records
72
+ WHERE beneficiary_id = ? AND created_at >= datetime('now','-30 days')
73
+ `).get(userId).total;
74
+ const earnedPrev30 = db.prepare(`
75
+ SELECT COALESCE(SUM(amount),0) as total FROM commission_records
76
+ WHERE beneficiary_id = ? AND created_at >= datetime('now','-60 days')
77
+ AND created_at < datetime('now','-30 days')
78
+ `).get(userId).total;
79
+ const wazLast30 = db.prepare(`
80
+ SELECT COALESCE(SUM(waz_amount),0) as total FROM binary_score_records
81
+ WHERE user_id = ? AND settled_at >= datetime('now','-30 days')
82
+ `).get(userId).total;
83
+ const projection = {
84
+ last_30_commission: earnedLast30,
85
+ prev_30_commission: earnedPrev30,
86
+ last_30_atomic_waz: wazLast30,
87
+ growth_rate: earnedPrev30 > 0 ? earnedLast30 / earnedPrev30 - 1 : null,
88
+ next_30_estimate: earnedLast30 + wazLast30,
89
+ };
90
+ 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);
103
+ if (lastInvite.t) {
104
+ const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
105
+ if (days > 14)
106
+ insights.push({ type: 'dormancy', level: 'warn', text: `${days} 天没有新直推 — 链接还在裤兜里?` });
107
+ else if (days < 3)
108
+ insights.push({ type: 'hot', level: 'success', text: `${days} 天前刚有新直推 — 趁热打铁` });
109
+ }
110
+ else if (l1 === 0) {
111
+ insights.push({ type: 'no_team', level: 'info', text: `还没有直推 — 先分享你买过且好评的商品给好友` });
112
+ }
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
+ if (!canL1Share && completedOrders === 0) {
119
+ insights.push({ type: 'unlock', level: 'info', text: `完成首笔购买可解锁分享奖励 — 当前仅可分享 PV 加人扩双轨树` });
120
+ }
121
+ if (shareableProducts.length > 0 && grand === 0) {
122
+ insights.push({ type: 'first_share', level: 'info', text: `你有 ${shareableProducts.length} 个可分享商品但暂无成交 — 试着把链接发给身边的人` });
123
+ }
124
+ const treeNode = (uid) => {
125
+ if (!uid)
126
+ 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);
128
+ if (!u)
129
+ return null;
130
+ return {
131
+ id: u.id, name: u.name,
132
+ lpv: Number(u.total_left_pv ?? 0),
133
+ rpv: Number(u.total_right_pv ?? 0),
134
+ left_id: u.left_child_id ?? null,
135
+ right_id: u.right_child_id ?? null,
136
+ };
137
+ };
138
+ const me_node = treeNode(userId);
139
+ const left_node = treeNode(myUser?.left_child_id);
140
+ const right_node = treeNode(myUser?.right_child_id);
141
+ const binaryTree = {
142
+ me: me_node,
143
+ left: left_node,
144
+ 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),
149
+ };
150
+ const meCard = db.prepare("SELECT permanent_code, handle FROM users WHERE id = ?").get(userId);
151
+ const codeForLink = meCard?.permanent_code || userId;
152
+ res.json({
153
+ user_id: userId,
154
+ permanent_code: meCard?.permanent_code || null,
155
+ handle: meCard?.handle || null,
156
+ referral_link: `${req.protocol}://${req.get('host')}/i/${codeForLink}`,
157
+ region: me?.region || 'global',
158
+ my_sponsor: mySponsor ? { id: me.sponsor_id, name: mySponsor.name } : null,
159
+ permissions: {
160
+ can_l1_share: canL1Share,
161
+ completed_orders: completedOrders,
162
+ l1_share_override: overrideRow?.l1_share_override ?? 0,
163
+ reason: canL1Share
164
+ ? (overrideRow?.l1_share_override === 1 ? 'admin_grant' : 'verified_buyer')
165
+ : 'need_completed_order',
166
+ },
167
+ team: { l1, l2, l3, total: l1 + l2 + l3 },
168
+ earnings: {
169
+ grand_total: grand,
170
+ l1: byLevel[1],
171
+ l2: byLevel[2],
172
+ l3: byLevel[3],
173
+ },
174
+ recent,
175
+ shareable_products: shareableProducts,
176
+ projection,
177
+ insights,
178
+ 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`,
181
+ total_left_pv: Number(myUser?.total_left_pv ?? 0),
182
+ total_right_pv: Number(myUser?.total_right_pv ?? 0),
183
+ left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
184
+ right_child: myUser?.right_child_id ? { id: myUser.right_child_id, name: rightChildName } : null,
185
+ my_placement: myUser?.placement_id ? { id: myUser.placement_id, name: myPlacementName, side: myUser.placement_side } : null,
186
+ score: scoreAgg,
187
+ recent_binary: recentBinary,
188
+ tier_config: tiers,
189
+ binary_tree: binaryTree,
190
+ },
191
+ });
192
+ });
193
+ // 直推 L1 列表
194
+ app.get('/api/promoter/team', (req, res) => {
195
+ const user = auth(req, res);
196
+ if (!user)
197
+ return;
198
+ const userId = user.id;
199
+ const rows = db.prepare(`
200
+ SELECT u.id, u.name, u.created_at, u.region,
201
+ (SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as their_l1,
202
+ COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = ? AND source_buyer_id = u.id), 0) as my_earned_from_them
203
+ FROM users u WHERE u.sponsor_id = ?
204
+ ORDER BY u.created_at DESC LIMIT 100
205
+ `).all(userId, userId);
206
+ res.json({ team: rows });
207
+ });
208
+ }
@@ -0,0 +1,227 @@
1
+ export function registerPublicUtilsRoutes(app, deps) {
2
+ const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
3
+ app.get('/api/health', (_req, res) => {
4
+ const t0 = Date.now();
5
+ let dbOk = false;
6
+ let dbLatency = 0;
7
+ try {
8
+ const t1 = Date.now();
9
+ const r = db.prepare('SELECT 1 as ok').get();
10
+ dbLatency = Date.now() - t1;
11
+ dbOk = r?.ok === 1;
12
+ }
13
+ catch {
14
+ dbOk = false;
15
+ }
16
+ const uptime = Math.floor((Date.now() - SERVICE_START_MS) / 1000);
17
+ const healthy = dbOk;
18
+ res.status(healthy ? 200 : 503).json({
19
+ status: healthy ? 'ok' : 'degraded',
20
+ uptime_sec: uptime,
21
+ db: { ok: dbOk, latency_ms: dbLatency },
22
+ seed_strength: MASTER_SEED === 'webaz-dev-seed-changeme' ? 'default' : MASTER_SEED.length >= 32 ? 'strong' : 'weak',
23
+ env: NODE_ENV,
24
+ check_ms: Date.now() - t0,
25
+ });
26
+ });
27
+ app.post('/api/mcp-telemetry', (req, res) => {
28
+ const ip = req.ip || 'unknown';
29
+ if (!rateLimitOk(ip))
30
+ return void res.status(429).json({ error: 'rate-limited' });
31
+ const { tool_name, outcome, latency_ms, user_id_hash, server_version } = req.body ?? {};
32
+ if (typeof tool_name !== 'string' || tool_name.length === 0 || tool_name.length > 64) {
33
+ return void res.status(400).json({ error: 'bad tool_name' });
34
+ }
35
+ if (outcome !== 'success' && outcome !== 'error') {
36
+ return void res.status(400).json({ error: 'bad outcome' });
37
+ }
38
+ const lat = Number(latency_ms);
39
+ if (!Number.isFinite(lat) || lat < 0 || lat > 60_000) {
40
+ return void res.status(400).json({ error: 'bad latency' });
41
+ }
42
+ const uih = typeof user_id_hash === 'string' && /^[0-9a-f]{1,32}$/.test(user_id_hash) ? user_id_hash : null;
43
+ const sv = typeof server_version === 'string' && server_version.length <= 32 ? server_version : null;
44
+ try {
45
+ db.prepare(`
46
+ INSERT INTO mcp_tool_calls (tool_name, user_id_hash, server_version, outcome, latency_ms)
47
+ VALUES (?, ?, ?, ?, ?)
48
+ `).run(tool_name, uih, sv, outcome, Math.round(lat));
49
+ }
50
+ catch { /* swallow — never fail telemetry */ }
51
+ res.json({ ok: true });
52
+ });
53
+ app.get('/api/system-flags', (_req, res) => {
54
+ const requireRef = db.prepare("SELECT value FROM system_state WHERE key='require_ref_to_register'").get()?.value === '1';
55
+ const inviteRotation = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
56
+ // #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
57
+ const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
58
+ res.json({
59
+ require_ref_to_register: requireRef,
60
+ invite_rotation_enabled: inviteRotation,
61
+ turnstile_site_key: turnstileSiteKey,
62
+ });
63
+ });
64
+ // #1045 + #1048 整合 — 公开诚实化 manifest
65
+ // /.well-known/webaz-protocol.json — 标准 well-known URL,任何 HTTP 客户端可发现
66
+ // /api/protocol-status — JSON API 别名(同份内容)
67
+ // 内容:network_state(协议处于哪一阶段 + 诚实免责) + issuers(信任锚地址 + 轮换历史)
68
+ // 信任锚是 Phase 4 凭证(/api/me/agents/:prefix/passport)的验签依据,陌生第三方靠这个端点找到"webaz 官方地址"。
69
+ function buildProtocolManifest() {
70
+ const phase = db.prepare("SELECT value FROM system_state WHERE key='protocol_phase'").get()?.value || 'pre_launch';
71
+ // real_users = 已绑 Passkey 的账号数(我们的"真人"定义);pre-launch 应当 ≈0
72
+ const realUsers = db.prepare("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials").get()?.n ?? 0;
73
+ const issuerActiveSince = db.prepare("SELECT value FROM system_state WHERE key='issuer_active_since'").get()?.value || '2026-05-30';
74
+ return {
75
+ name: 'WebAZ Protocol',
76
+ schema_version: 1,
77
+ network_state: {
78
+ phase,
79
+ real_users_on_canonical: realUsers,
80
+ canonical_endpoint: 'https://webaz.xyz',
81
+ economic_flow: 'simulated WAZ (test currency, 1 WAZ ≈ 1 USDC peg is a模拟基准, not a real exchange rate). No fiat/crypto settlement yet.',
82
+ disclaimer: {
83
+ zh: '本协议尚未公开上线,真实用户(已绑 Passkey 的账号)数实时反映在 real_users_on_canonical。无真实经济流转。请勿据此评估市场规模、做投资决策、或替终端用户承诺任何经济关系。',
84
+ en: 'Protocol is pre-launch; real_users_on_canonical reflects live count of Passkey-bound accounts. No real economic settlement yet. Do not use this data to evaluate market size, make investment decisions, or commit to third parties on a user\'s behalf.',
85
+ },
86
+ },
87
+ issuers: {
88
+ // 数组结构 — 将来轮换/泄露时往里追加新条目并把旧的设 revoked_at,验真方按签发时间判落在哪个有效区间
89
+ agent_passport: [
90
+ {
91
+ address: issuerAddress(),
92
+ did_web: 'did:web:webaz.xyz', // W3C DID method,resolve at /.well-known/did.json
93
+ did_legacy: 'did:webaz:' + issuerAddress(), // 原自定义形态,Phase 4 webaz_format 仍用
94
+ scheme: 'eip191',
95
+ purpose: 'Phase 4 Agent Passport signing (custodian_fingerprint + risk_score + engagement_depth + behavior_profile)',
96
+ active_since: issuerActiveSince,
97
+ revoked_at: null,
98
+ verify: 'verifyMessage(address, passport.canonical, passport.signature) — any party can ecrecover without calling WebAZ',
99
+ },
100
+ ],
101
+ },
102
+ // 公开披露文档(#1050) — 协议层"钱怎么流"的源真理(协议外可读)
103
+ disclosures: {
104
+ economic_model: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md',
105
+ mlm_compliance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md',
106
+ agent_governance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
107
+ protocol_compatibility: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/PROTOCOL-COMPATIBILITY-AUDIT-2026-05-30.md',
108
+ changelog: 'https://github.com/seasonsagents-art/webaz/blob/main/CHANGELOG.md',
109
+ },
110
+ // 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
111
+ roadmap: {
112
+ philosophy: 'Disclose what is done + what is known-not-yet-done. We do not commit to deadlines (pre-launch). We DO commit to honest enumeration.',
113
+ completed: [
114
+ 'Phase 1-4 Agent Passport: custodian + risk + behavior + signed portable export (cross-protocol ecrecover verifiable)',
115
+ 'Phase 3a-3d access control: registration rate-limit + invite-required + declared-scope reads/writes + non-Passkey writers must declare',
116
+ 'Iron-Rule: arbitrate / vote / agent_revoke / delete_passkey / large withdraw all require live WebAuthn ceremony',
117
+ 'Integrity disclosure: MCP descriptions + /.well-known + pre-launch banner + protocol-status endpoint',
118
+ 'Cross-user read daily cap — distinct other-user-id per day, Passkey humans capped too (#1043, 2026-05-30)',
119
+ 'AP2 Mandate dual-output — verify_price + place_order emit signed AP2 Intent/Cart/Payment Mandate alongside webaz format (B.4 b, 2026-05-30)',
120
+ 'Public economic model document — docs/ECONOMIC-MODEL.md (#1050, 2026-05-30)',
121
+ ],
122
+ known_next: [
123
+ 'Registration captcha or email verification (anti-sybil last mile) — task #1049, pre-launch follow-up',
124
+ 'Phase 5 ZK privacy L2/L3 — long-term research; triggered when real cross-protocol consumers appear',
125
+ ],
126
+ deliberate_deferrals: [
127
+ 'Model B (independent sub-agent keys with delegated scope) — destination locked; not building until real multi-agent demand surfaces',
128
+ 'Parameterized fee rates (settleOrder currently hardcodes) — wire when fee governance launches',
129
+ 'Binary/PV referral region MLM-term cleanup — region-gated to max=3 areas; deferred until structural decision on tree',
130
+ ],
131
+ rationale_for_no_dates: 'pre-launch protocols that commit to dates either lie or rush. We commit only to a published list and to honesty about its state.',
132
+ },
133
+ };
134
+ }
135
+ app.get('/.well-known/webaz-protocol.json', (_req, res) => {
136
+ res.setHeader('Cache-Control', 'public, max-age=300'); // 5min 边缘缓存,降轮询
137
+ res.json(buildProtocolManifest());
138
+ });
139
+ app.get('/api/protocol-status', (_req, res) => {
140
+ res.setHeader('Cache-Control', 'public, max-age=300');
141
+ res.json(buildProtocolManifest());
142
+ });
143
+ // W3C DID Document(B.6 b DID 短期 mapping,2026-05-30):
144
+ // did:web:webaz.xyz 通过 HTTPS 解析到这里(W3C did:web spec §3.2)
145
+ // verificationMethod 用 EcdsaSecp256k1RecoveryMethod2020 + CAIP-10 blockchainAccountId
146
+ // 任何标准 DID resolver(Veramo / SpruceID / KILT / web5 ...)可 GET → 解出 issuer key → 验 Phase 4 凭证签名
147
+ app.get('/.well-known/did.json', (_req, res) => {
148
+ const addr = issuerAddress();
149
+ res.setHeader('Cache-Control', 'public, max-age=300');
150
+ res.json({
151
+ '@context': [
152
+ 'https://www.w3.org/ns/did/v1',
153
+ 'https://w3id.org/security/suites/secp256k1recovery-2020/v2',
154
+ ],
155
+ id: 'did:web:webaz.xyz',
156
+ verificationMethod: [
157
+ {
158
+ id: 'did:web:webaz.xyz#key-1',
159
+ type: 'EcdsaSecp256k1RecoveryMethod2020',
160
+ controller: 'did:web:webaz.xyz',
161
+ // CAIP-10:eip155 namespace + Base mainnet chain id (8453) + 同把 hot wallet 地址(Phase 4 同款)
162
+ // 注:WAZ peg USDC,真链 Base/Base Sepolia,但 issuer key 是协议级,链中立,这里只标声明绑定到 Base 用于消费者发现
163
+ blockchainAccountId: `eip155:8453:${addr}`,
164
+ },
165
+ ],
166
+ assertionMethod: ['did:web:webaz.xyz#key-1'],
167
+ authentication: ['did:web:webaz.xyz#key-1'],
168
+ // 自描述 — 让 resolver 知道这个 DID 用来签 webaz agent passport
169
+ service: [
170
+ {
171
+ id: 'did:web:webaz.xyz#agent-passport-endpoint',
172
+ type: 'WebAZAgentPassportEndpoint',
173
+ serviceEndpoint: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport',
174
+ },
175
+ {
176
+ id: 'did:web:webaz.xyz#protocol-manifest',
177
+ type: 'WebAZProtocolManifest',
178
+ serviceEndpoint: 'https://webaz.xyz/.well-known/webaz-protocol.json',
179
+ },
180
+ ],
181
+ });
182
+ });
183
+ app.get('/api/editor-picks', (_req, res) => {
184
+ const products = db.prepare(`
185
+ SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
186
+ p.title as product_title, p.price, p.images, p.category,
187
+ u.handle as seller_handle
188
+ FROM editor_picks ep
189
+ JOIN products p ON p.id = ep.target_id AND p.status = 'active'
190
+ JOIN users u ON u.id = p.seller_id
191
+ WHERE ep.kind = 'product' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
192
+ ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
193
+ `).all();
194
+ const sellers = db.prepare(`
195
+ SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
196
+ u.handle, u.name, u.shop_banner_url, u.bio
197
+ FROM editor_picks ep
198
+ JOIN users u ON u.id = ep.target_id AND u.role = 'seller'
199
+ WHERE ep.kind = 'seller' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
200
+ ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
201
+ `).all();
202
+ res.json({ products, sellers });
203
+ });
204
+ app.get('/api/manifest', (_req, res) => {
205
+ res.json(generateManifest(db));
206
+ });
207
+ const _errorReportLimiter = new Map();
208
+ app.post('/api/error-report', (req, res) => {
209
+ const ip = req.ip || 'unknown';
210
+ const now = Date.now();
211
+ const bucket = _errorReportLimiter.get(ip) || { count: 0, reset: now + 60_000 };
212
+ if (now > bucket.reset) {
213
+ bucket.count = 0;
214
+ bucket.reset = now + 60_000;
215
+ }
216
+ bucket.count++;
217
+ _errorReportLimiter.set(ip, bucket);
218
+ if (bucket.count > 30)
219
+ return void res.status(429).json({ error: 'rate_limited' });
220
+ const { message, stack, url } = req.body || {};
221
+ if (!message || typeof message !== 'string')
222
+ return void res.status(400).json({ error: 'message required' });
223
+ const user = getUser(req);
224
+ logError('client', message.slice(0, 1000), { stack: String(stack || '').slice(0, 4000), url: String(url || '').slice(0, 500), user_agent: req.headers['user-agent'] || '', user_id: user?.id });
225
+ res.json({ ok: true });
226
+ });
227
+ }
@@ -0,0 +1,54 @@
1
+ /** web-push 失败回调:删除已失效订阅。导出以备 P1-5 任务调用。 */
2
+ export function cleanupStaleSubscription(db, endpoint) {
3
+ if (!endpoint)
4
+ return;
5
+ db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint);
6
+ }
7
+ export function registerPushRoutes(app, deps) {
8
+ const { db, generateId, auth, vapidPublicKey } = deps;
9
+ app.get('/api/push/vapid-public-key', (_req, res) => {
10
+ if (!vapidPublicKey)
11
+ return void res.status(503).json({ error: '推送未配置,请联系管理员设置 VAPID_PUBLIC_KEY' });
12
+ res.json({ key: vapidPublicKey });
13
+ });
14
+ app.post('/api/push/subscribe', (req, res) => {
15
+ const user = auth(req, res);
16
+ if (!user)
17
+ return;
18
+ const { endpoint, keys, user_agent } = req.body || {};
19
+ if (!endpoint || !keys?.p256dh || !keys?.auth) {
20
+ return void res.status(400).json({ error: '订阅参数不完整(需 endpoint + keys.p256dh + keys.auth)' });
21
+ }
22
+ const id = generateId('psub');
23
+ // 同 user + endpoint 视作重新订阅
24
+ const existing = db.prepare('SELECT id FROM push_subscriptions WHERE user_id = ? AND endpoint = ?').get(user.id, String(endpoint));
25
+ 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);
28
+ return void res.json({ success: true, id: existing.id });
29
+ }
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
+ res.json({ success: true, id });
33
+ });
34
+ app.delete('/api/push/subscribe', (req, res) => {
35
+ const user = auth(req, res);
36
+ if (!user)
37
+ return;
38
+ const { endpoint } = req.body || {};
39
+ if (endpoint) {
40
+ db.prepare('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?').run(user.id, String(endpoint));
41
+ }
42
+ else {
43
+ db.prepare('DELETE FROM push_subscriptions WHERE user_id = ?').run(user.id);
44
+ }
45
+ res.json({ success: true });
46
+ });
47
+ app.get('/api/push/status', (req, res) => {
48
+ const user = auth(req, res);
49
+ if (!user)
50
+ return;
51
+ const cnt = db.prepare('SELECT COUNT(*) as n FROM push_subscriptions WHERE user_id = ? AND enabled = 1').get(user.id).n;
52
+ res.json({ subscribed: cnt > 0, count: cnt, vapid_configured: !!vapidPublicKey });
53
+ });
54
+ }