@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.
- package/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3679 -852
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31362 -2320
- package/dist/pwa/public/i18n.js +5308 -111
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +4 -1
- package/dist/pwa/public/manifest.json +39 -4
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +278 -5
- package/dist/pwa/public/sw.js +41 -2
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +386 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/ap2-mandate.js +121 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +161 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +106 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +382 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +227 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9317 -2097
- 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
|
+
}
|