@seasonkoh/webaz 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +165 -64
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +54 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +7 -5
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +147 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +33 -30
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +33 -17
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +14 -16
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +59 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -60
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +74 -36
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
|
@@ -1,61 +1,63 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerPromoterRoutes(app, deps) {
|
|
2
3
|
const { db, auth, isAllowedSponsor } = deps;
|
|
3
|
-
|
|
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 =
|
|
9
|
-
const l2 =
|
|
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
|
-
|
|
13
|
-
const l3 =
|
|
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
|
-
|
|
19
|
-
const earned =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
36
|
-
const me =
|
|
37
|
-
const mySponsor = me?.sponsor_id ?
|
|
38
|
-
const myUser =
|
|
39
|
-
const leftChildName = myUser?.left_child_id ?
|
|
40
|
-
const rightChildName = myUser?.right_child_id ?
|
|
41
|
-
const myPlacementName = myUser?.placement_id ?
|
|
42
|
-
const scoreAgg =
|
|
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
|
-
|
|
49
|
-
const recentBinary =
|
|
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
|
-
|
|
54
|
-
const tiers =
|
|
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 =
|
|
57
|
-
const overrideRow =
|
|
58
|
-
const shareableProducts =
|
|
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
|
-
|
|
70
|
-
const earnedLast30 =
|
|
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
|
-
|
|
74
|
-
const earnedPrev30 =
|
|
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
|
-
|
|
79
|
-
const wazLast30 =
|
|
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
|
-
|
|
84
|
+
`, [userId])).total;
|
|
83
85
|
const projection = {
|
|
84
86
|
last_30_commission: earnedLast30,
|
|
85
87
|
prev_30_commission: earnedPrev30,
|
|
@@ -99,7 +101,7 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
99
101
|
else
|
|
100
102
|
insights.push({ type: 'balanced', level: 'success', text: `双腿均衡度 ${(ratio * 100).toFixed(0)}% — 对碰节奏健康` });
|
|
101
103
|
}
|
|
102
|
-
const lastInvite =
|
|
104
|
+
const lastInvite = (await dbOne(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`, [userId]));
|
|
103
105
|
if (lastInvite.t) {
|
|
104
106
|
const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
|
|
105
107
|
if (days > 14)
|
|
@@ -121,10 +123,10 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
121
123
|
if (shareableProducts.length > 0 && grand === 0) {
|
|
122
124
|
insights.push({ type: 'first_share', level: 'info', text: `你有 ${shareableProducts.length} 个可分享商品但暂无成交 — 试着把链接发给身边的人` });
|
|
123
125
|
}
|
|
124
|
-
const treeNode = (uid) => {
|
|
126
|
+
const treeNode = async (uid) => {
|
|
125
127
|
if (!uid)
|
|
126
128
|
return null;
|
|
127
|
-
const u =
|
|
129
|
+
const u = await dbOne("SELECT id, name, total_left_pv, total_right_pv, left_child_id, right_child_id FROM users WHERE id = ?", [uid]);
|
|
128
130
|
if (!u)
|
|
129
131
|
return null;
|
|
130
132
|
return {
|
|
@@ -135,25 +137,29 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
135
137
|
right_id: u.right_child_id ?? null,
|
|
136
138
|
};
|
|
137
139
|
};
|
|
138
|
-
const me_node = treeNode(userId);
|
|
139
|
-
const left_node = treeNode(myUser?.left_child_id);
|
|
140
|
-
const right_node = treeNode(myUser?.right_child_id);
|
|
140
|
+
const me_node = await treeNode(userId);
|
|
141
|
+
const left_node = await treeNode(myUser?.left_child_id);
|
|
142
|
+
const right_node = await treeNode(myUser?.right_child_id);
|
|
141
143
|
const binaryTree = {
|
|
142
144
|
me: me_node,
|
|
143
145
|
left: left_node,
|
|
144
146
|
right: right_node,
|
|
145
|
-
ll: treeNode(left_node?.left_id),
|
|
146
|
-
lr: treeNode(left_node?.right_id),
|
|
147
|
-
rl: treeNode(right_node?.left_id),
|
|
148
|
-
rr: treeNode(right_node?.right_id),
|
|
147
|
+
ll: await treeNode(left_node?.left_id),
|
|
148
|
+
lr: await treeNode(left_node?.right_id),
|
|
149
|
+
rl: await treeNode(right_node?.left_id),
|
|
150
|
+
rr: await treeNode(right_node?.right_id),
|
|
149
151
|
};
|
|
150
|
-
const meCard =
|
|
151
|
-
|
|
152
|
+
const meCard = await dbOne("SELECT permanent_code, handle FROM users WHERE id = ?", [userId]);
|
|
153
|
+
// invite links use permanent_code ONLY — never fall back to user_id (would leak usr_xxx into ?ref).
|
|
154
|
+
const codeForLink = meCard?.permanent_code || null;
|
|
155
|
+
const host = `${req.protocol}://${req.get('host')}`;
|
|
152
156
|
res.json({
|
|
153
157
|
user_id: userId,
|
|
154
158
|
permanent_code: meCard?.permanent_code || null,
|
|
155
159
|
handle: meCard?.handle || null,
|
|
156
|
-
|
|
160
|
+
invite_code_available: !!codeForLink,
|
|
161
|
+
referral_link: codeForLink ? `${host}/i/${codeForLink}` : null,
|
|
162
|
+
invite_unavailable_reason: codeForLink ? null : 'permanent_code_missing — refresh or contact support',
|
|
157
163
|
region: me?.region || 'global',
|
|
158
164
|
my_sponsor: mySponsor ? { id: me.sponsor_id, name: mySponsor.name } : null,
|
|
159
165
|
permissions: {
|
|
@@ -176,8 +182,8 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
176
182
|
projection,
|
|
177
183
|
insights,
|
|
178
184
|
atomic: {
|
|
179
|
-
left_invite_url: `${
|
|
180
|
-
right_invite_url: `${
|
|
185
|
+
left_invite_url: codeForLink ? `${host}/i/${codeForLink}-L` : null,
|
|
186
|
+
right_invite_url: codeForLink ? `${host}/i/${codeForLink}-R` : null,
|
|
181
187
|
total_left_pv: Number(myUser?.total_left_pv ?? 0),
|
|
182
188
|
total_right_pv: Number(myUser?.total_right_pv ?? 0),
|
|
183
189
|
left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
|
|
@@ -191,18 +197,18 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
191
197
|
});
|
|
192
198
|
});
|
|
193
199
|
// 直推 L1 列表
|
|
194
|
-
app.get('/api/promoter/team', (req, res) => {
|
|
200
|
+
app.get('/api/promoter/team', async (req, res) => {
|
|
195
201
|
const user = auth(req, res);
|
|
196
202
|
if (!user)
|
|
197
203
|
return;
|
|
198
204
|
const userId = user.id;
|
|
199
|
-
const rows =
|
|
205
|
+
const rows = await dbAll(`
|
|
200
206
|
SELECT u.id, u.name, u.created_at, u.region,
|
|
201
207
|
(SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as their_l1,
|
|
202
208
|
COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = ? AND source_buyer_id = u.id), 0) as my_earned_from_them
|
|
203
209
|
FROM users u WHERE u.sponsor_id = ?
|
|
204
210
|
ORDER BY u.created_at DESC LIMIT 100
|
|
205
|
-
|
|
211
|
+
`, [userId, userId]);
|
|
206
212
|
res.json({ team: rows });
|
|
207
213
|
});
|
|
208
214
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
|
|
2
|
+
export function registerPublicBuildTasksRoutes(app, deps) {
|
|
3
|
+
const { db, errorRes } = deps;
|
|
4
|
+
app.get('/api/public/build-tasks', (req, res) => {
|
|
5
|
+
const v = validateTaskFilters(req.query);
|
|
6
|
+
if (!v.ok)
|
|
7
|
+
return void errorRes(res, 400, v.code, v.detail); // fail-closed: bad filter → typed 400
|
|
8
|
+
const tasks = listBuildTasksWithAgentMetadata(db, v.filters, 'public');
|
|
9
|
+
res.json(withContributionReadEnvelope({ tasks }));
|
|
10
|
+
});
|
|
11
|
+
app.get('/api/public/build-tasks/:id', (req, res) => {
|
|
12
|
+
// 'public' scope already restricts to audience=public + status=open; a non-visible task returns null →
|
|
13
|
+
// 404 (same as truly-missing, so existence of a restricted/internal task is never disclosed).
|
|
14
|
+
const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'public');
|
|
15
|
+
if (!task)
|
|
16
|
+
return void errorRes(res, 404, 'NOT_FOUND', '任务不存在');
|
|
17
|
+
res.json(withContributionReadEnvelope({ task }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { SOFTWARE_VERSION, CONTRACT_VERSION } from '../../version.js';
|
|
5
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
5
6
|
import { capabilityMatrix } from '../endpoint-actions.js';
|
|
6
7
|
import { buildEntityDictionary } from '../entity-dictionary.js';
|
|
7
8
|
import { buildGoalIndex } from '../goal-index.js';
|
|
@@ -13,13 +14,13 @@ import { buildNegativeSpace } from '../negative-space.js';
|
|
|
13
14
|
import { buildAcpProductFeed } from '../acp-feed.js';
|
|
14
15
|
export function registerPublicUtilsRoutes(app, deps) {
|
|
15
16
|
const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
|
|
16
|
-
app.get('/api/health', (_req, res) => {
|
|
17
|
+
app.get('/api/health', async (_req, res) => {
|
|
17
18
|
const t0 = Date.now();
|
|
18
19
|
let dbOk = false;
|
|
19
20
|
let dbLatency = 0;
|
|
20
21
|
try {
|
|
21
22
|
const t1 = Date.now();
|
|
22
|
-
const r =
|
|
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
|
-
|
|
59
|
+
await dbRun(`
|
|
59
60
|
INSERT INTO mcp_tool_calls (tool_name, user_id_hash, server_version, outcome, latency_ms)
|
|
60
61
|
VALUES (?, ?, ?, ?, ?)
|
|
61
|
-
|
|
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 =
|
|
68
|
-
const inviteRotation =
|
|
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 =
|
|
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 =
|
|
86
|
-
const issuerActiveSince =
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
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(
|
|
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
|
|
293
|
-
// 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)
|
|
294
|
-
// (ACP /complete 是卡+PSP,WebAZ 未接)
|
|
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 =
|
|
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
|
-
`)
|
|
352
|
-
const sellers =
|
|
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
|
-
`)
|
|
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 =
|
|
372
|
-
const verifierCount =
|
|
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 =
|
|
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 =
|
|
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',
|
package/dist/pwa/routes/push.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
7
|
+
await dbRun('DELETE FROM push_subscriptions WHERE endpoint = ?', [endpoint]);
|
|
6
8
|
}
|
|
7
9
|
export function registerPushRoutes(app, deps) {
|
|
8
|
-
|
|
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 =
|
|
27
|
+
const existing = await dbOne('SELECT id FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
|
|
25
28
|
if (existing) {
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
await dbRun('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
|
|
41
42
|
}
|
|
42
43
|
else {
|
|
43
|
-
|
|
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 =
|
|
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
|
}
|