@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- 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 +288 -208
- 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 +182 -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 +11 -3
- 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-discovery.js +55 -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-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -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/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -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 +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- 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-atomic.js +10 -4
- 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 +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- 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 +65 -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 +32 -7
- 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 +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- 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 +164 -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 +34 -31
- 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 +51 -29
- 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 +20 -19
- 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 +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- 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 +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- 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 +60 -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 +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- 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 +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- 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 -70
- 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 +75 -37
- 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 +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerVerifyTasksRoutes(app, deps) {
|
|
3
|
+
// 只读/单写站点走 RFC-016 异步 seam;db 保留:submit 是"提交→封顶→结算"裁决资金路径,
|
|
4
|
+
// 提交 CAS + 计票 + seal-CAS 必须原子(db.transaction);settleTask(发奖/扣权,server.ts 无状态门)
|
|
5
|
+
// 在 tx 提交后只对真正封顶的那一票触发,防并发双结算双发奖。Phase 3 迁 pg 行锁。
|
|
2
6
|
const { db, auth, assignVerifiers, settleTask, getVerifierStats } = deps;
|
|
3
7
|
// 卖家确认:已在原平台添加验证码 → 任务进入分配池
|
|
4
|
-
app.post('/api/verify-tasks/:id/confirm', (req, res) => {
|
|
8
|
+
app.post('/api/verify-tasks/:id/confirm', async (req, res) => {
|
|
5
9
|
const user = auth(req, res);
|
|
6
10
|
if (!user)
|
|
7
11
|
return;
|
|
8
|
-
const task =
|
|
12
|
+
const task = await dbOne(`SELECT * FROM verify_tasks WHERE id = ? AND status IN ('code_issued','open')`, [req.params.id]);
|
|
9
13
|
if (!task)
|
|
10
14
|
return void res.json({ error: '任务不存在或已结束' });
|
|
11
|
-
const product =
|
|
15
|
+
const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [task.product_id]);
|
|
12
16
|
if (!product || product.seller_id !== user.id)
|
|
13
17
|
return void res.status(403).json({ error: '无权限' });
|
|
14
18
|
if (task.status === 'open') {
|
|
15
19
|
return void res.json({ success: true, already_open: true, message: '任务已在验证中,无需重复确认' });
|
|
16
20
|
}
|
|
17
|
-
|
|
21
|
+
await dbRun(`UPDATE verify_tasks SET status='open' WHERE id=?`, [req.params.id]);
|
|
18
22
|
try {
|
|
19
23
|
assignVerifiers(req.params.id);
|
|
20
24
|
}
|
|
@@ -22,26 +26,26 @@ export function registerVerifyTasksRoutes(app, deps) {
|
|
|
22
26
|
res.json({ success: true, message: '任务已提交到验证池,等待审核员确认' });
|
|
23
27
|
});
|
|
24
28
|
// 卖家:查询某商品的进行中验证任务(供编辑页展示验证码)
|
|
25
|
-
app.get('/api/verify-tasks/by-product/:productId', (req, res) => {
|
|
29
|
+
app.get('/api/verify-tasks/by-product/:productId', async (req, res) => {
|
|
26
30
|
const user = auth(req, res);
|
|
27
31
|
if (!user)
|
|
28
32
|
return;
|
|
29
|
-
const product =
|
|
33
|
+
const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [req.params.productId]);
|
|
30
34
|
if (!product || product.seller_id !== user.id)
|
|
31
35
|
return void res.status(403).json({ error: '无权限' });
|
|
32
|
-
const tasks =
|
|
36
|
+
const tasks = await dbAll(`
|
|
33
37
|
SELECT id, type, url, code, status, expires_at, created_at,
|
|
34
38
|
(SELECT COUNT(*) FROM verify_submissions WHERE task_id = verify_tasks.id AND submitted_at IS NOT NULL) as submissions_done
|
|
35
39
|
FROM verify_tasks WHERE product_id = ? AND status IN ('code_issued','open') ORDER BY created_at DESC
|
|
36
|
-
|
|
40
|
+
`, [req.params.productId]);
|
|
37
41
|
res.json(tasks);
|
|
38
42
|
});
|
|
39
43
|
// 卖家:查询我发起的所有认领任务(用于"查看任务进度"页)
|
|
40
|
-
app.get('/api/verify-tasks/my-claims', (req, res) => {
|
|
44
|
+
app.get('/api/verify-tasks/my-claims', async (req, res) => {
|
|
41
45
|
const user = auth(req, res);
|
|
42
46
|
if (!user)
|
|
43
47
|
return;
|
|
44
|
-
const tasks =
|
|
48
|
+
const tasks = await dbAll(`
|
|
45
49
|
SELECT vt.id, vt.type, vt.url, vt.code, vt.status, vt.result,
|
|
46
50
|
vt.verifiers_needed, vt.expires_at, vt.created_at, vt.settled_at,
|
|
47
51
|
p.title as product_title, p.id as product_id,
|
|
@@ -51,14 +55,14 @@ export function registerVerifyTasksRoutes(app, deps) {
|
|
|
51
55
|
WHERE p.seller_id = ?
|
|
52
56
|
ORDER BY vt.created_at DESC
|
|
53
57
|
LIMIT 30
|
|
54
|
-
|
|
58
|
+
`, [user.id]);
|
|
55
59
|
res.json(tasks);
|
|
56
60
|
});
|
|
57
|
-
app.get('/api/verify-tasks/mine', (req, res) => {
|
|
61
|
+
app.get('/api/verify-tasks/mine', async (req, res) => {
|
|
58
62
|
const user = auth(req, res);
|
|
59
63
|
if (!user)
|
|
60
64
|
return;
|
|
61
|
-
const tasks =
|
|
65
|
+
const tasks = await dbAll(`
|
|
62
66
|
SELECT vt.id, vt.type, vt.url, vt.verifiers_needed, vt.reward_per_verifier, vt.expires_at,
|
|
63
67
|
vs.id as sub_id, vs.submitted_at, vs.verdict,
|
|
64
68
|
(SELECT COUNT(*) FROM verify_submissions WHERE task_id = vt.id AND submitted_at IS NOT NULL) as submissions_done
|
|
@@ -66,40 +70,60 @@ export function registerVerifyTasksRoutes(app, deps) {
|
|
|
66
70
|
JOIN verify_submissions vs ON vs.task_id = vt.id AND vs.verifier_id = ?
|
|
67
71
|
WHERE vt.status = 'open'
|
|
68
72
|
ORDER BY vt.created_at DESC
|
|
69
|
-
|
|
73
|
+
`, [user.id]);
|
|
70
74
|
const stats = getVerifierStats(user.id);
|
|
71
75
|
res.json({ tasks, stats });
|
|
72
76
|
});
|
|
73
77
|
// 验证者:提交验证结果(填入式)
|
|
74
|
-
app.post('/api/verify-tasks/:id/submit', (req, res) => {
|
|
78
|
+
app.post('/api/verify-tasks/:id/submit', async (req, res) => {
|
|
75
79
|
const user = auth(req, res);
|
|
76
80
|
if (!user)
|
|
77
81
|
return;
|
|
78
82
|
const { submission } = req.body;
|
|
79
|
-
const sub =
|
|
80
|
-
.get(req.params.id, user.id);
|
|
83
|
+
const sub = await dbOne(`SELECT * FROM verify_submissions WHERE task_id = ? AND verifier_id = ?`, [req.params.id, user.id]);
|
|
81
84
|
if (!sub)
|
|
82
85
|
return void res.json({ error: '未分配到此任务' });
|
|
83
86
|
if (sub.submitted_at)
|
|
84
87
|
return void res.json({ error: '已提交过' });
|
|
85
|
-
const task =
|
|
88
|
+
const task = await dbOne('SELECT * FROM verify_tasks WHERE id = ? AND status = ?', [req.params.id, 'open']);
|
|
86
89
|
if (!task)
|
|
87
90
|
return void res.json({ error: '任务已结束或不存在' });
|
|
88
91
|
if (new Date(task.expires_at) < new Date())
|
|
89
92
|
return void res.json({ error: '任务已过期' });
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
93
|
+
// 裁决原子段:CAS 写本验证者未提交的行(防同人并发双提交)→ 计票 → 达标则 CAS 翻 open→settling。
|
|
94
|
+
// 返回 didReach=true 仅给真正把任务翻到结算态的那一票。
|
|
95
|
+
const submissionText = (submission ?? '').trim();
|
|
96
|
+
let didReach = false;
|
|
97
|
+
try {
|
|
98
|
+
didReach = db.transaction(() => {
|
|
99
|
+
const upd = db.prepare(`UPDATE verify_submissions SET submission=?, submitted_at=datetime('now') WHERE task_id=? AND verifier_id=? AND submitted_at IS NULL`)
|
|
100
|
+
.run(submissionText, req.params.id, user.id);
|
|
101
|
+
if (upd.changes === 0)
|
|
102
|
+
throw new Error('ALREADY_SUBMITTED');
|
|
103
|
+
const doneCount = db.prepare(`SELECT COUNT(*) as n FROM verify_submissions WHERE task_id = ? AND submitted_at IS NOT NULL`).get(req.params.id).n;
|
|
104
|
+
if (doneCount < task.verifiers_needed)
|
|
105
|
+
return false;
|
|
106
|
+
const seal = db.prepare(`UPDATE verify_tasks SET status='settling' WHERE id=? AND status='open'`).run(req.params.id);
|
|
107
|
+
return seal.changes === 1;
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
if (e.message === 'ALREADY_SUBMITTED')
|
|
112
|
+
return void res.json({ error: '已提交过' });
|
|
113
|
+
console.error('[verify-tasks submit tx]', e.message);
|
|
114
|
+
return void res.status(500).json({ error: '提交失败,请重试' });
|
|
115
|
+
}
|
|
116
|
+
// 结算在事务提交后只对触发封顶的那一票执行(settleTask 自身写 status='settled' + 发奖)。
|
|
117
|
+
if (didReach)
|
|
94
118
|
settleTask(req.params.id);
|
|
95
119
|
res.json({ success: true, message: '提交成功,等待其他验证者完成后自动结算' });
|
|
96
120
|
});
|
|
97
121
|
// 公开验证大厅 — 仅显示分配给我的未提交任务
|
|
98
|
-
app.get('/api/verify-tasks/open', (req, res) => {
|
|
122
|
+
app.get('/api/verify-tasks/open', async (req, res) => {
|
|
99
123
|
const user = auth(req, res);
|
|
100
124
|
if (!user)
|
|
101
125
|
return;
|
|
102
|
-
const tasks =
|
|
126
|
+
const tasks = await dbAll(`
|
|
103
127
|
SELECT vt.id, vt.type, vt.url, vt.reward_per_verifier, vt.expires_at,
|
|
104
128
|
(SELECT COUNT(*) FROM verify_submissions WHERE task_id=vt.id AND submitted_at IS NOT NULL) as done,
|
|
105
129
|
vt.verifiers_needed
|
|
@@ -108,7 +132,7 @@ export function registerVerifyTasksRoutes(app, deps) {
|
|
|
108
132
|
WHERE vt.status = 'open'
|
|
109
133
|
ORDER BY vt.created_at ASC
|
|
110
134
|
LIMIT 10
|
|
111
|
-
|
|
135
|
+
`, [user.id]);
|
|
112
136
|
res.json(tasks);
|
|
113
137
|
});
|
|
114
138
|
app.get('/api/verify-stats', (req, res) => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerWaitlistRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, isTrustedRole, errorRes } = deps;
|
|
5
|
+
app.post('/api/products/:product_id/waitlist', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
if (isTrustedRole(user))
|
|
8
10
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
9
|
-
const p =
|
|
11
|
+
const p = await dbOne('SELECT id, seller_id, status, stock FROM products WHERE id = ?', [req.params.product_id]);
|
|
10
12
|
if (!p)
|
|
11
13
|
return void res.status(404).json({ error: '商品不存在' });
|
|
12
14
|
if (p.seller_id === user.id)
|
|
@@ -17,22 +19,21 @@ export function registerWaitlistRoutes(app, deps) {
|
|
|
17
19
|
return void res.status(400).json({ error: '商品有货,无需排队 — 直接下单即可' });
|
|
18
20
|
const qty = Math.max(1, Math.min(99, Number(req.body?.desired_qty) || 1));
|
|
19
21
|
const note = req.body?.note ? String(req.body.note).slice(0, 200) : null;
|
|
20
|
-
|
|
21
|
-
.run(user.id, req.params.product_id, qty, note);
|
|
22
|
+
await dbRun(`INSERT OR REPLACE INTO product_waitlist (user_id, product_id, desired_qty, note) VALUES (?,?,?,?)`, [user.id, req.params.product_id, qty, note]);
|
|
22
23
|
res.json({ success: true });
|
|
23
24
|
});
|
|
24
|
-
app.delete('/api/products/:product_id/waitlist', (req, res) => {
|
|
25
|
+
app.delete('/api/products/:product_id/waitlist', async (req, res) => {
|
|
25
26
|
const user = auth(req, res);
|
|
26
27
|
if (!user)
|
|
27
28
|
return;
|
|
28
|
-
|
|
29
|
+
await dbRun('DELETE FROM product_waitlist WHERE user_id = ? AND product_id = ?', [user.id, req.params.product_id]);
|
|
29
30
|
res.json({ success: true });
|
|
30
31
|
});
|
|
31
|
-
app.get('/api/waitlist', (req, res) => {
|
|
32
|
+
app.get('/api/waitlist', async (req, res) => {
|
|
32
33
|
const user = auth(req, res);
|
|
33
34
|
if (!user)
|
|
34
35
|
return;
|
|
35
|
-
const rows =
|
|
36
|
+
const rows = await dbAll(`
|
|
36
37
|
SELECT w.product_id, w.desired_qty, w.note, w.notified_at, w.created_at,
|
|
37
38
|
p.title, p.price, p.stock, p.status as product_status, p.category,
|
|
38
39
|
u.name as seller_name, u.handle as seller_handle
|
|
@@ -41,25 +42,25 @@ export function registerWaitlistRoutes(app, deps) {
|
|
|
41
42
|
JOIN users u ON u.id = p.seller_id
|
|
42
43
|
WHERE w.user_id = ?
|
|
43
44
|
ORDER BY w.created_at DESC LIMIT 200
|
|
44
|
-
|
|
45
|
+
`, [user.id]);
|
|
45
46
|
res.json({ items: rows });
|
|
46
47
|
});
|
|
47
|
-
app.get('/api/products/:product_id/waitlist/check', (req, res) => {
|
|
48
|
+
app.get('/api/products/:product_id/waitlist/check', async (req, res) => {
|
|
48
49
|
const user = auth(req, res);
|
|
49
50
|
if (!user)
|
|
50
51
|
return;
|
|
51
|
-
const exists =
|
|
52
|
+
const exists = await dbOne('SELECT 1 FROM product_waitlist WHERE user_id = ? AND product_id = ?', [user.id, req.params.product_id]);
|
|
52
53
|
res.json({ in_waitlist: !!exists });
|
|
53
54
|
});
|
|
54
55
|
// seller 查 waitlist count(决定备多少货)
|
|
55
|
-
app.get('/api/products/:product_id/waitlist/count', (req, res) => {
|
|
56
|
+
app.get('/api/products/:product_id/waitlist/count', async (req, res) => {
|
|
56
57
|
const user = auth(req, res);
|
|
57
58
|
if (!user)
|
|
58
59
|
return;
|
|
59
|
-
const p =
|
|
60
|
+
const p = await dbOne('SELECT seller_id FROM products WHERE id = ?', [req.params.product_id]);
|
|
60
61
|
if (!p || p.seller_id !== user.id)
|
|
61
62
|
return void res.status(403).json({ error: '仅卖家可看' });
|
|
62
|
-
const r =
|
|
63
|
+
const r = (await dbOne(`SELECT COUNT(*) as cnt, COALESCE(SUM(desired_qty), 0) as total_qty FROM product_waitlist WHERE product_id = ? AND notified_at IS NULL`, [req.params.product_id]));
|
|
63
64
|
res.json({ pending_users: r.cnt, total_desired: r.total_qty });
|
|
64
65
|
});
|
|
65
66
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerWalletReadRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, isTrustedRole, generateId, verifyPassword, deriveDepositAddress, getProtocolParam, getPublicClient, getIsMainnet, getActiveChainId, getUsdcContract, getNetwork } = deps;
|
|
3
5
|
// 钱包状态
|
|
4
|
-
app.get('/api/wallet', (req, res) => {
|
|
6
|
+
app.get('/api/wallet', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
8
10
|
if (isTrustedRole(user))
|
|
9
11
|
return void res.status(403).json({ error: '受信角色无钱包', error_code: 'TRUSTED_ROLE_NO_WALLET' });
|
|
10
|
-
const wallet =
|
|
12
|
+
const wallet = await dbOne('SELECT * FROM wallets WHERE user_id = ?', [user.id]);
|
|
11
13
|
if (!wallet)
|
|
12
14
|
return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
|
|
13
15
|
if (!wallet.deposit_address) {
|
|
14
16
|
const addr = deriveDepositAddress(user.id);
|
|
15
|
-
|
|
17
|
+
await dbRun('UPDATE wallets SET deposit_address = ? WHERE user_id = ?', [addr, user.id]);
|
|
16
18
|
wallet.deposit_address = addr;
|
|
17
19
|
}
|
|
18
20
|
res.json(wallet);
|
|
@@ -22,7 +24,7 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
22
24
|
const user = auth(req, res);
|
|
23
25
|
if (!user)
|
|
24
26
|
return;
|
|
25
|
-
const wallet =
|
|
27
|
+
const wallet = await dbOne('SELECT deposit_address FROM wallets WHERE user_id = ?', [user.id]);
|
|
26
28
|
if (!wallet?.deposit_address)
|
|
27
29
|
return void res.status(404).json({ error: '充值地址未生成' });
|
|
28
30
|
try {
|
|
@@ -52,16 +54,16 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
52
54
|
});
|
|
53
55
|
});
|
|
54
56
|
// 白名单 GET / POST / DELETE
|
|
55
|
-
app.get('/api/wallet/whitelist', (req, res) => {
|
|
57
|
+
app.get('/api/wallet/whitelist', async (req, res) => {
|
|
56
58
|
const user = auth(req, res);
|
|
57
59
|
if (!user)
|
|
58
60
|
return;
|
|
59
|
-
const rows =
|
|
61
|
+
const rows = await dbAll(`
|
|
60
62
|
SELECT id, address, label, added_at, activates_at
|
|
61
63
|
FROM withdrawal_whitelist
|
|
62
64
|
WHERE user_id = ? AND revoked_at IS NULL
|
|
63
65
|
ORDER BY added_at DESC
|
|
64
|
-
|
|
66
|
+
`, [user.id]);
|
|
65
67
|
const now = Date.now();
|
|
66
68
|
res.json({
|
|
67
69
|
whitelist: rows.map(r => ({
|
|
@@ -70,7 +72,7 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
70
72
|
})),
|
|
71
73
|
});
|
|
72
74
|
});
|
|
73
|
-
app.post('/api/wallet/whitelist', (req, res) => {
|
|
75
|
+
app.post('/api/wallet/whitelist', async (req, res) => {
|
|
74
76
|
const user = auth(req, res);
|
|
75
77
|
if (!user)
|
|
76
78
|
return;
|
|
@@ -85,39 +87,38 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
85
87
|
return void res.json({ error: '请输入有效的以太坊地址' });
|
|
86
88
|
// P0-1: 统一小写存储
|
|
87
89
|
const address = addressRaw.toLowerCase();
|
|
88
|
-
const existing =
|
|
90
|
+
const existing = await dbOne(`SELECT id, revoked_at FROM withdrawal_whitelist WHERE user_id = ? AND address = ?`, [user.id, address]);
|
|
89
91
|
if (existing && !existing.revoked_at)
|
|
90
92
|
return void res.json({ error: '该地址已在白名单中' });
|
|
91
93
|
const id = generateId('wl');
|
|
92
94
|
const activatesAt = new Date(Date.now() + 24 * 3600_000).toISOString().slice(0, 19).replace('T', ' ');
|
|
93
95
|
if (existing) {
|
|
94
|
-
|
|
96
|
+
await dbRun(`UPDATE withdrawal_whitelist
|
|
95
97
|
SET revoked_at = NULL, added_at = datetime('now'), activates_at = ?, label = ?, id = ?
|
|
96
|
-
WHERE user_id = ? AND address =
|
|
97
|
-
.run(activatesAt, label || null, id, user.id, address);
|
|
98
|
+
WHERE user_id = ? AND address = ?`, [activatesAt, label || null, id, user.id, address]);
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
100
|
-
|
|
101
|
-
VALUES (?,?,?,?,?)
|
|
101
|
+
await dbRun(`INSERT INTO withdrawal_whitelist (id, user_id, address, label, activates_at)
|
|
102
|
+
VALUES (?,?,?,?,?)`, [id, user.id, address, label || null, activatesAt]);
|
|
102
103
|
}
|
|
103
104
|
res.json({ ok: true, id, activates_at: activatesAt, message: '地址已添加,24 小时冷却期后可用于提现' });
|
|
104
105
|
});
|
|
105
|
-
app.delete('/api/wallet/whitelist/:id', (req, res) => {
|
|
106
|
+
app.delete('/api/wallet/whitelist/:id', async (req, res) => {
|
|
106
107
|
const user = auth(req, res);
|
|
107
108
|
if (!user)
|
|
108
109
|
return;
|
|
109
|
-
const row =
|
|
110
|
+
const row = await dbOne(`SELECT user_id FROM withdrawal_whitelist WHERE id = ?`, [req.params.id]);
|
|
110
111
|
if (!row || row.user_id !== user.id)
|
|
111
112
|
return void res.status(404).json({ error: '地址不存在' });
|
|
112
|
-
|
|
113
|
+
await dbRun(`UPDATE withdrawal_whitelist SET revoked_at = datetime('now') WHERE id = ?`, [req.params.id]);
|
|
113
114
|
res.json({ ok: true });
|
|
114
115
|
});
|
|
115
116
|
// 我的提现记录
|
|
116
|
-
app.get('/api/wallet/withdrawals', (req, res) => {
|
|
117
|
+
app.get('/api/wallet/withdrawals', async (req, res) => {
|
|
117
118
|
const user = auth(req, res);
|
|
118
119
|
if (!user)
|
|
119
120
|
return;
|
|
120
|
-
const list =
|
|
121
|
+
const list = await dbAll(`SELECT id, to_address, amount, status, created_at, tx_hash FROM withdrawal_requests WHERE user_id = ? ORDER BY created_at DESC LIMIT 10`, [user.id]);
|
|
121
122
|
res.json(list);
|
|
122
123
|
});
|
|
123
124
|
// 我的充值记录(含确认进度)
|
|
@@ -140,8 +141,8 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
140
141
|
const user = auth(req, res);
|
|
141
142
|
if (!user)
|
|
142
143
|
return;
|
|
143
|
-
const list =
|
|
144
|
-
FROM deposit_txns WHERE user_id = ? ORDER BY created_at DESC LIMIT 10
|
|
144
|
+
const list = await dbAll(`SELECT tx_hash, amount, credited_waz, block_number, swept, confirmed_at, block_at_seen, created_at
|
|
145
|
+
FROM deposit_txns WHERE user_id = ? ORDER BY created_at DESC LIMIT 10`, [user.id]);
|
|
145
146
|
const requiredConf = getProtocolParam('usdc_required_confirmations', 12);
|
|
146
147
|
let latestBlock = null;
|
|
147
148
|
if (list.some(r => !r.confirmed_at)) {
|
|
@@ -159,32 +160,32 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
159
160
|
});
|
|
160
161
|
res.json(enriched);
|
|
161
162
|
});
|
|
162
|
-
//
|
|
163
|
-
app.get('/api/wallet/income', (req, res) => {
|
|
163
|
+
// 收入构成:销售 / 分享归因 / PV 记录(pre-launch,若适用)
|
|
164
|
+
app.get('/api/wallet/income', async (req, res) => {
|
|
164
165
|
const user = auth(req, res);
|
|
165
166
|
if (!user)
|
|
166
167
|
return;
|
|
167
|
-
const commByLevel =
|
|
168
|
+
const commByLevel = await dbAll(`
|
|
168
169
|
SELECT level, COUNT(*) as cnt, COALESCE(SUM(amount),0) as total
|
|
169
170
|
FROM commission_records WHERE beneficiary_id = ? GROUP BY level
|
|
170
|
-
|
|
171
|
+
`, [user.id]);
|
|
171
172
|
const commMap = { l1: { count: 0, total: 0 }, l2: { count: 0, total: 0 }, l3: { count: 0, total: 0 } };
|
|
172
173
|
for (const r of commByLevel) {
|
|
173
174
|
const key = `l${r.level}`;
|
|
174
175
|
if (commMap[key])
|
|
175
176
|
commMap[key] = { count: r.cnt, total: Number(r.total.toFixed(2)) };
|
|
176
177
|
}
|
|
177
|
-
const binary =
|
|
178
|
+
const binary = (await dbOne(`
|
|
178
179
|
SELECT
|
|
179
180
|
COUNT(CASE WHEN settled_at IS NOT NULL THEN 1 END) as settled_cnt,
|
|
180
181
|
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount END), 0) as settled_waz,
|
|
181
182
|
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score END), 0) as pending_score
|
|
182
183
|
FROM binary_score_records WHERE user_id = ?
|
|
183
|
-
|
|
184
|
-
const sales =
|
|
184
|
+
`, [user.id]));
|
|
185
|
+
const sales = (await dbOne(`
|
|
185
186
|
SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount),0) as total
|
|
186
187
|
FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
187
|
-
|
|
188
|
+
`, [user.id]));
|
|
188
189
|
const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total +
|
|
189
190
|
Number(binary.settled_waz) + Number(sales.total);
|
|
190
191
|
res.json({
|
|
@@ -199,20 +200,57 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
// P0 测试充值(Phase 0 专用)
|
|
202
|
-
|
|
203
|
+
// 测试水龙头(faucet)— 资金铸造路径,fail-safe **默认关闭**(Codex #187/#222 / task #1128)。
|
|
204
|
+
// 1) 环境门禁(见 isFaucetAllowed):**绝不靠 NODE_ENV 的默认值**关闭铸币路径 —— 本仓库不设 NODE_ENV,
|
|
205
|
+
// 生产全靠 Railway 面板注入;旧代码 `NODE_ENV || 'development'` 若漏设会把 faucet 在生产打开。
|
|
206
|
+
// 现在:显式 production → 永远关;部署平台(Railway 注入 RAILWAY_ENVIRONMENT)→ 默认关,
|
|
207
|
+
// 仅显式 WEBAZ_ENABLE_TEST_FAUCET=1 才开;本地(无平台信号 + dev/test/未设)→ 开。
|
|
208
|
+
// 2) cap 原子化:单条 `balance = MIN(5000, balance + ?)`,并发/Phase 3 pg 下都不会越过 5000。
|
|
209
|
+
// 3) Phase 3 资金路径:迁 pg 时应移到 wallet-write + SELECT...FOR UPDATE 行锁。
|
|
210
|
+
const FAUCET_ALLOWED = isFaucetAllowed({
|
|
211
|
+
nodeEnv: process.env.NODE_ENV,
|
|
212
|
+
onDeployPlatform: !!(process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID || process.env.RAILWAY_SERVICE_ID),
|
|
213
|
+
explicitEnableFlag: process.env.WEBAZ_ENABLE_TEST_FAUCET === '1',
|
|
214
|
+
});
|
|
215
|
+
app.post('/api/wallet/topup', async (req, res) => {
|
|
203
216
|
const user = auth(req, res);
|
|
204
217
|
if (!user)
|
|
205
218
|
return;
|
|
219
|
+
if (!FAUCET_ALLOWED)
|
|
220
|
+
return void res.status(403).json({ error: '充值水龙头仅在测试环境开放', error_code: 'FAUCET_DISABLED' });
|
|
206
221
|
if (isTrustedRole(user))
|
|
207
222
|
return void res.status(403).json({ error: '受信角色无钱包', error_code: 'TRUSTED_ROLE_NO_WALLET' });
|
|
208
223
|
const amount = Math.min(1000, Math.max(1, Number(req.body?.amount) || 500));
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
224
|
+
const before = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
225
|
+
if (!before)
|
|
211
226
|
return void res.status(500).json({ error: '钱包记录缺失', error_code: 'WALLET_MISSING' });
|
|
212
|
-
if (
|
|
227
|
+
if (before.balance >= 5000)
|
|
213
228
|
return void res.json({ error: '余额已达上限 5000 WAZ,无需充值' });
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
// 原子封顶:无论并发,balance 永不超 5000(MIN 在单条 UPDATE 内对当前值求值)。
|
|
230
|
+
await dbRun('UPDATE wallets SET balance = MIN(5000, balance + ?) WHERE user_id = ?', [amount, user.id]);
|
|
231
|
+
const after = (await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id])).balance;
|
|
232
|
+
res.json({ success: true, added: Math.round((after - before.balance) * 100) / 100, new_balance: after, capped: after >= 5000 });
|
|
217
233
|
});
|
|
218
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Faucet (WAZ mint path) gate — fail-safe DEFAULT-CLOSED (task #1128 / Codex #187/#222).
|
|
237
|
+
*
|
|
238
|
+
* The repo never sets NODE_ENV; production relies on the Railway dashboard injecting it. So a
|
|
239
|
+
* mint path must NOT lean on a `NODE_ENV || 'development'` default to stay shut — an unset
|
|
240
|
+
* NODE_ENV in prod would open it. Rules (in order):
|
|
241
|
+
* 1. explicit NODE_ENV==='production' → CLOSED (even if the enable flag is mis-set).
|
|
242
|
+
* 2. explicit enable flag (WEBAZ_ENABLE_TEST_FAUCET=1) → OPEN (staging/test boxes opt in).
|
|
243
|
+
* 3. on a deploy platform (Railway injects RAILWAY_ENVIRONMENT/PROJECT/SERVICE) without the
|
|
244
|
+
* flag → CLOSED — this is the fail-safe for a misconfigured/unset NODE_ENV in prod.
|
|
245
|
+
* 4. off-platform (local dev) → OPEN only for unset / 'development' / 'test'; any other
|
|
246
|
+
* explicit value → CLOSED.
|
|
247
|
+
*/
|
|
248
|
+
export function isFaucetAllowed(e) {
|
|
249
|
+
if (e.nodeEnv === 'production')
|
|
250
|
+
return false;
|
|
251
|
+
if (e.explicitEnableFlag)
|
|
252
|
+
return true;
|
|
253
|
+
if (e.onDeployPlatform)
|
|
254
|
+
return false;
|
|
255
|
+
return e.nodeEnv === undefined || e.nodeEnv === 'development' || e.nodeEnv === 'test';
|
|
256
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { verifyMessage } from 'viem';
|
|
2
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
export function registerWalletWriteRoutes(app, deps) {
|
|
3
4
|
const { db, auth, isTrustedRole, generateId, getProtocolParam, consumeGateToken, issueCode, findActiveCode, maskEmail, LARGE_WITHDRAW_THRESHOLD } = deps;
|
|
4
5
|
// Wave G-1: 签名挑战 — 5min 一次性 nonce
|
|
@@ -59,20 +60,20 @@ export function registerWalletWriteRoutes(app, deps) {
|
|
|
59
60
|
}
|
|
60
61
|
// 已通过签名校验 → 加入白名单,免 24h 冷却(activates_at = NOW)
|
|
61
62
|
const addrLc = String(address).toLowerCase();
|
|
62
|
-
const existing =
|
|
63
|
+
const existing = await dbOne('SELECT id, revoked_at FROM withdrawal_whitelist WHERE user_id = ? AND address = ?', [user.id, addrLc]);
|
|
63
64
|
if (existing) {
|
|
64
|
-
|
|
65
|
-
signature_verified_at = datetime('now'), label = COALESCE(?, label) WHERE id =
|
|
66
|
-
.run(label || null, existing.id);
|
|
65
|
+
await dbRun(`UPDATE withdrawal_whitelist SET activates_at = datetime('now'), revoked_at = NULL,
|
|
66
|
+
signature_verified_at = datetime('now'), label = COALESCE(?, label) WHERE id = ?`, [label || null, existing.id]);
|
|
67
67
|
return void res.json({ success: true, id: existing.id, activated: true });
|
|
68
68
|
}
|
|
69
69
|
const id = generateId('wl');
|
|
70
|
-
|
|
71
|
-
VALUES (?,?,?,?,datetime('now'),datetime('now'))
|
|
72
|
-
.run(id, user.id, addrLc, label ? String(label).slice(0, 30) : null);
|
|
70
|
+
await dbRun(`INSERT INTO withdrawal_whitelist (id, user_id, address, label, activates_at, signature_verified_at)
|
|
71
|
+
VALUES (?,?,?,?,datetime('now'),datetime('now'))`, [id, user.id, addrLc, label ? String(label).slice(0, 30) : null]);
|
|
73
72
|
res.json({ success: true, id, activated: true });
|
|
74
73
|
});
|
|
75
74
|
// 提现申请
|
|
75
|
+
// RFC-016: withdraw + confirm 是铁律资金转出路径,余额扣减是裸顺序写(非 db.transaction)。
|
|
76
|
+
// 保持整体同步,Phase 3 随资金路径整体迁 pg(BEGIN + SELECT...FOR UPDATE 行锁),不在此引入 await 间隙。
|
|
76
77
|
app.post('/api/wallet/withdraw', (req, res) => {
|
|
77
78
|
const user = auth(req, res);
|
|
78
79
|
if (!user)
|
|
@@ -129,8 +130,10 @@ export function registerWalletWriteRoutes(app, deps) {
|
|
|
129
130
|
// 资金转出 = 真人在场(spec §4 铁律,与 vote/arbitrate/agent_revoke 同档)。
|
|
130
131
|
// email-OTP 在 agent 威胁模型下不足(agent 可读监护人收件箱);故弃用旧的"非 Passkey → email 兜底"路径。
|
|
131
132
|
// 未注册 Passkey 的账户:不能提现,先去「安全」绑 Passkey(pre-launch 0 真用户,推动资金操作 Passkey 化)。
|
|
132
|
-
|
|
133
|
-
if (
|
|
133
|
+
// Codex #100 P1:提现真人 Passkey 是【铁律】,绝不可被任何 protocol param 关闭 → 无条件执行,不读开关。
|
|
134
|
+
// (旧代码 if (require_human_presence_for_withdraw===1) 让 protocol admin 把它设 0 即可绕过铁律。)
|
|
135
|
+
// 该 param 已锁死 value=min=max=1,仅作展示(见 server.ts DEFAULT_PARAMS + 启动迁移)。
|
|
136
|
+
{
|
|
134
137
|
const hasPasskeyRow = db.prepare('SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?').get(user.id);
|
|
135
138
|
const hasPasskey = (hasPasskeyRow?.n || 0) > 0;
|
|
136
139
|
if (!hasPasskey) {
|