@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,9 +1,53 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAuthRegisterRoutes(app, deps) {
|
|
2
3
|
// VALID_REGIONS + INVITE_ROTATION_HANDLES 通过 deps.X 在 handler 内延迟读
|
|
3
4
|
// (server.ts 用 getter 注入;destructure at register-time would trigger TDZ 因为它们在下方 const)
|
|
4
|
-
const { db, errorRes, INTERNAL_AUDITOR_ID, isAllowedSponsor,
|
|
5
|
+
const { db, errorRes, INTERNAL_AUDITOR_ID, isAllowedSponsor, resolveInviteCodeRef, generateId, generateSecureKey, generatePermanentCode, deriveHandle, clientIpHash, clientUaHash, pickPreferredSide, joinPowerLeg, inviteRotationLookup, issueCode, findActiveCode, canDeliverCodes, emailDeliveryNotConfigured, recordSession, broadcastSystemEvent } = deps;
|
|
6
|
+
// CODE_TTL_MIN / MAX_CODE_ATTEMPTS 通过 deps.X 在 handler 内延迟读(它们在 server.ts 是后置 const,
|
|
7
|
+
// register-time destructure 会触发 TDZ)。
|
|
8
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
9
|
+
const normEmail = (raw) => String(raw || '').trim().toLowerCase();
|
|
10
|
+
// IP 级发码限流(5/min)— 防爆破列举邮箱 / 刷验证码
|
|
11
|
+
const regCodeHits = new Map();
|
|
12
|
+
// 注册邮箱验证:发码到邮箱(purpose='register',无账号故 user_id='')。
|
|
13
|
+
// 注册场景需明确告知"邮箱已占用"(无法防枚举,标准取舍),但限流 + captcha 兜底。
|
|
14
|
+
app.post('/api/register/send-code', async (req, res) => {
|
|
15
|
+
const email = normEmail(req.body?.email);
|
|
16
|
+
if (!email || !EMAIL_RE.test(email))
|
|
17
|
+
return void errorRes(res, 400, 'EMAIL_INVALID', '请填写有效邮箱');
|
|
18
|
+
if (!canDeliverCodes()) {
|
|
19
|
+
const u = emailDeliveryNotConfigured();
|
|
20
|
+
return void res.status(u.status).json({ error: u.error, error_code: u.error_code });
|
|
21
|
+
}
|
|
22
|
+
const ip = req.ip || '';
|
|
23
|
+
if (ip) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const rec = regCodeHits.get(ip);
|
|
26
|
+
if (rec && now - rec.firstAt < 60_000) {
|
|
27
|
+
rec.count++;
|
|
28
|
+
if (rec.count > 5)
|
|
29
|
+
return void errorRes(res, 429, 'CODE_RATE_LIMITED', '发送过于频繁,请 1 分钟后再试');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
regCodeHits.set(ip, { count: 1, firstAt: now });
|
|
33
|
+
}
|
|
34
|
+
if (regCodeHits.size > 1000) {
|
|
35
|
+
for (const [k, v] of regCodeHits)
|
|
36
|
+
if (now - v.firstAt > 60_000)
|
|
37
|
+
regCodeHits.delete(k);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const dup = await dbOne("SELECT 1 FROM users WHERE lower(email) = ? AND email_verified = 1 AND id NOT IN ('sys_protocol', ?) LIMIT 1", [email, INTERNAL_AUDITOR_ID]);
|
|
41
|
+
if (dup)
|
|
42
|
+
return void errorRes(res, 409, 'EMAIL_TAKEN', '该邮箱已注册,请直接登录或用 #recover 找回');
|
|
43
|
+
const issued = await issueCode('', 'email', email, 'register');
|
|
44
|
+
if (!issued.ok) {
|
|
45
|
+
return void res.status(issued.status || 503).json({ error: issued.error || '验证码发送失败,请稍后再试', error_code: issued.error_code || 'EMAIL_DELIVERY_FAILED' });
|
|
46
|
+
}
|
|
47
|
+
res.json({ success: true, notice: '验证码已发送至邮箱,请查收(含垃圾箱)', expires_in_min: deps.CODE_TTL_MIN });
|
|
48
|
+
});
|
|
5
49
|
app.post('/api/register', async (req, res) => {
|
|
6
|
-
const { name, role, sponsor_id, region, placement_inviter_id,
|
|
50
|
+
const { name, role, sponsor_id, region, placement_inviter_id, turnstile_token } = req.body;
|
|
7
51
|
const validRoles = ['buyer', 'seller'];
|
|
8
52
|
if (!name?.trim())
|
|
9
53
|
return void errorRes(res, 400, 'NAME_REQUIRED', '请填写名称');
|
|
@@ -39,11 +83,37 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
39
83
|
const trimmed = name.trim();
|
|
40
84
|
if (trimmed.length < 2 || trimmed.length > 40)
|
|
41
85
|
return void errorRes(res, 400, 'NAME_LENGTH', '名称长度需在 2–40 个字符之间');
|
|
42
|
-
const dup =
|
|
86
|
+
const dup = await dbOne("SELECT 1 FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?) LIMIT 1", [trimmed, INTERNAL_AUDITOR_ID]);
|
|
43
87
|
if (dup)
|
|
44
88
|
return void errorRes(res, 409, 'NAME_TAKEN', '该名称已被占用,请换一个');
|
|
89
|
+
// ── 邮箱验证优先注册 ────────────────────────────────────────
|
|
90
|
+
// PWA 人类路径强制:必须先 /register/send-code 拿验证码,提交时带 email + code,校验通过才建号 + email_verified=1。
|
|
91
|
+
// 这样每个新 PWA 账号天生有 verified 邮箱 → #recover 永远可用。
|
|
92
|
+
// agent/MCP 注册走 handleRegister 直插库(自托管 key,不需邮件找回),不经此端点,不受影响。
|
|
93
|
+
const email = normEmail(req.body?.email);
|
|
94
|
+
const code = String(req.body?.code || '').trim();
|
|
95
|
+
if (!email || !code)
|
|
96
|
+
return void errorRes(res, 400, 'EMAIL_VERIFICATION_REQUIRED', '注册需先验证邮箱:请填写邮箱并输入收到的验证码');
|
|
97
|
+
if (!EMAIL_RE.test(email))
|
|
98
|
+
return void errorRes(res, 400, 'EMAIL_INVALID', '邮箱格式不正确');
|
|
99
|
+
const emailDup = await dbOne("SELECT 1 FROM users WHERE lower(email) = ? AND email_verified = 1 AND id NOT IN ('sys_protocol', ?) LIMIT 1", [email, INTERNAL_AUDITOR_ID]);
|
|
100
|
+
if (emailDup)
|
|
101
|
+
return void errorRes(res, 409, 'EMAIL_TAKEN', '该邮箱已注册,请直接登录或用 #recover 找回');
|
|
102
|
+
// 校验注册验证码(按 email 查,purpose='register')。错码计数,超限作废,均【不建号】。
|
|
103
|
+
const codeRow = findActiveCode('email', email, 'register');
|
|
104
|
+
if (!codeRow)
|
|
105
|
+
return void errorRes(res, 400, 'CODE_EXPIRED', '验证码已过期或未发送,请重新获取');
|
|
106
|
+
if (String(codeRow.code) !== code) {
|
|
107
|
+
const attempts = Number(codeRow.attempts || 0) + 1;
|
|
108
|
+
if (attempts >= deps.MAX_CODE_ATTEMPTS) {
|
|
109
|
+
await dbRun("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?", [attempts, codeRow.id]);
|
|
110
|
+
return void errorRes(res, 400, 'CODE_TOO_MANY', '错误次数过多,验证码已作废,请重新获取');
|
|
111
|
+
}
|
|
112
|
+
await dbRun("UPDATE verification_codes SET attempts = ? WHERE id = ?", [attempts, codeRow.id]);
|
|
113
|
+
return void errorRes(res, 400, 'CODE_INVALID', `验证码错误(剩余 ${deps.MAX_CODE_ATTEMPTS - attempts} 次)`);
|
|
114
|
+
}
|
|
45
115
|
const ROLE_WHITELIST = [];
|
|
46
|
-
const requireRef =
|
|
116
|
+
const requireRef = (await dbOne("SELECT value FROM system_state WHERE key='require_ref_to_register'", []))?.value === '1';
|
|
47
117
|
// 2026-05-30 合规复审:取消 china 豁免——D1b 引入时保留 china 通道是为获客,
|
|
48
118
|
// 但合规上正好反向(china 是 MLM 风险最高地区,豁免反而把最高风险地区做成最容易进)。
|
|
49
119
|
// 全球统一需邀请。第三方尽调报告风险 1 + 问题 1 的回应。
|
|
@@ -53,20 +123,18 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
53
123
|
let sponsorId = null;
|
|
54
124
|
let sponsorPath = null;
|
|
55
125
|
let sponsorSkipped = null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
sponsorRawRef = sufM[1];
|
|
61
|
-
suffixSide = sufM[2].toLowerCase() === 'l' ? 'left' : 'right';
|
|
62
|
-
}
|
|
126
|
+
// invite codes ONLY: 6-7 char permanent_code with optional -L/-R side. usr_xxx / @handle / handle are
|
|
127
|
+
// no longer accepted as a registration sponsor (anti-ambiguity; narrows the public invite surface).
|
|
128
|
+
const sponsorRawRef = (sponsor_id && typeof sponsor_id === 'string') ? sponsor_id.trim() : '';
|
|
129
|
+
let resolvedSponsorId = null;
|
|
63
130
|
if (sponsorRawRef) {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
66
|
-
return void errorRes(res, 400, 'INVALID_SPONSOR_REF', `邀请码无效:${sponsorRawRef.slice(0, 24)}
|
|
131
|
+
const ref = resolveInviteCodeRef(sponsorRawRef);
|
|
132
|
+
if (!ref) {
|
|
133
|
+
return void errorRes(res, 400, 'INVALID_SPONSOR_REF', `邀请码无效:${sponsorRawRef.slice(0, 24)}(仅接受 6-7 位永久码;请检查或留空跳过)`);
|
|
67
134
|
}
|
|
68
|
-
|
|
69
|
-
|
|
135
|
+
resolvedSponsorId = ref.userId;
|
|
136
|
+
// pre-public: 去左右码 — 邀请码自带的 -L/-R 侧别一律忽略,放置永远自动(见下)
|
|
137
|
+
const sponsor = await dbOne("SELECT id, sponsor_path FROM users WHERE id = ?", [ref.userId]);
|
|
70
138
|
if (!sponsor) {
|
|
71
139
|
return void errorRes(res, 400, 'INVALID_SPONSOR_REF', `邀请码对应用户不存在:${sponsorRawRef.slice(0, 24)}`);
|
|
72
140
|
}
|
|
@@ -90,21 +158,29 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
90
158
|
const ipHash = clientIpHash(req);
|
|
91
159
|
const uaHash = clientUaHash(req);
|
|
92
160
|
// Phase 3a:注册限频 — 同 IP 每小时最多 5 个新账号(挡批量刷号)
|
|
93
|
-
const recentReg =
|
|
161
|
+
const recentReg = (await dbOne(`SELECT COUNT(*) AS n FROM registration_audit_log WHERE ip_hash = ? AND created_at > datetime('now','-1 hour')`, [ipHash])).n;
|
|
94
162
|
if (recentReg >= 5) {
|
|
95
163
|
return void errorRes(res, 429, 'REGISTER_RATE_LIMITED', '注册过于频繁 — 同一网络每小时最多 5 个新账号,请稍后再试');
|
|
96
164
|
}
|
|
97
165
|
const registerTx = db.transaction(() => {
|
|
98
|
-
db.prepare(`INSERT INTO users (id, name, role, roles, api_key, sponsor_id, sponsor_path, region, permanent_code, handle)
|
|
99
|
-
VALUES (
|
|
100
|
-
.run(id, trimmed, role, JSON.stringify([role]), apiKey, sponsorId, sponsorPath, userRegion, permaCode, userHandle);
|
|
166
|
+
db.prepare(`INSERT INTO users (id, name, role, roles, api_key, sponsor_id, sponsor_path, region, permanent_code, handle, email, email_verified)
|
|
167
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,1)`)
|
|
168
|
+
.run(id, trimmed, role, JSON.stringify([role]), apiKey, sponsorId, sponsorPath, userRegion, permaCode, userHandle, email);
|
|
169
|
+
// 消费注册验证码(单次性)— 与建号同一事务,失败则整体回滚,不留"码已用但没建号"
|
|
170
|
+
db.prepare("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?").run(codeRow.id);
|
|
101
171
|
db.prepare('INSERT INTO wallets (user_id, balance) VALUES (?,1000)').run(id);
|
|
102
172
|
db.prepare(`INSERT INTO registration_audit_log (user_id, ip_hash, ua_hash, sponsor_id) VALUES (?,?,?,?)`)
|
|
103
173
|
.run(id, ipHash, uaHash, sponsorId);
|
|
104
|
-
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
174
|
+
// placement inviter is invite-code-only too (the sponsor code, or an explicit placement_inviter_id code)
|
|
175
|
+
let effectiveInviter = resolvedSponsorId;
|
|
176
|
+
// pre-public 去左右码:不再接受用户/邀请码指定的左右侧,放置侧别永远由系统自动决定(pickPreferredSide)
|
|
177
|
+
let effectiveSide = null;
|
|
178
|
+
if (placement_inviter_id) {
|
|
179
|
+
const p = resolveInviteCodeRef(String(placement_inviter_id));
|
|
180
|
+
if (p) {
|
|
181
|
+
effectiveInviter = p.userId;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
108
184
|
if (effectiveInviter && !effectiveSide) {
|
|
109
185
|
try {
|
|
110
186
|
effectiveSide = pickPreferredSide(effectiveInviter);
|
|
@@ -118,10 +194,15 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
118
194
|
const inviter = db.prepare("SELECT id FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?) LIMIT 1")
|
|
119
195
|
.get(effectiveInviter, INTERNAL_AUDITOR_ID);
|
|
120
196
|
if (inviter) {
|
|
197
|
+
// do NOT swallow: a known inviter+side that fails to place would leave a placement orphan
|
|
198
|
+
// (sponsor recorded but absent from the binary tree). Fail-closed → rethrow so the whole
|
|
199
|
+
// registration transaction rolls back (no users / wallet / audit rows persist).
|
|
121
200
|
try {
|
|
122
201
|
placement = joinPowerLeg(inviter.id, effectiveSide, id);
|
|
123
202
|
}
|
|
124
|
-
catch {
|
|
203
|
+
catch (e) {
|
|
204
|
+
throw new Error('PLACEMENT_FAILED:' + e.message);
|
|
205
|
+
}
|
|
125
206
|
}
|
|
126
207
|
}
|
|
127
208
|
const rotationEnabled = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
|
|
@@ -141,7 +222,10 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
141
222
|
txResult = registerTx();
|
|
142
223
|
}
|
|
143
224
|
catch (e) {
|
|
144
|
-
|
|
225
|
+
const msg = e.message;
|
|
226
|
+
console.error('[register-tx]', msg);
|
|
227
|
+
if (msg.startsWith('PLACEMENT_FAILED:'))
|
|
228
|
+
return void errorRes(res, 409, 'PLACEMENT_FAILED', '注册挂靠失败,请重试或联系支持(未创建账号)');
|
|
145
229
|
return void res.status(500).json({ error: '注册写入失败,请重试' });
|
|
146
230
|
}
|
|
147
231
|
const { placement, effectiveInviter, effectiveSide } = txResult;
|
|
@@ -158,6 +242,7 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
158
242
|
sponsor_id: sponsorId, region: userRegion,
|
|
159
243
|
permanent_code: permaCode,
|
|
160
244
|
handle: userHandle,
|
|
245
|
+
email, email_verified: true,
|
|
161
246
|
placement: placement ? { inviter_id: effectiveInviter, side: effectiveSide, depth: placement.depth } : null,
|
|
162
247
|
...(sponsorSkipped ? { sponsor_skipped: sponsorSkipped } : {}),
|
|
163
248
|
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAuthSessionsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, verifyPassword, recordSession, generateSecureKey } = deps;
|
|
5
|
+
app.get('/api/auth/sessions', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
const currentKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
|
|
8
|
-
const rows =
|
|
10
|
+
const rows = await dbAll(`
|
|
9
11
|
SELECT id, ip, user_agent, fingerprint_hash, created_at, last_seen_at, revoked_at, api_key
|
|
10
12
|
FROM user_sessions WHERE user_id = ? AND revoked_at IS NULL
|
|
11
13
|
ORDER BY last_seen_at DESC LIMIT 30
|
|
12
|
-
|
|
14
|
+
`, [user.id]);
|
|
13
15
|
res.json({
|
|
14
16
|
sessions: rows.map(r => ({
|
|
15
17
|
id: r.id,
|
|
@@ -23,23 +25,23 @@ export function registerAuthSessionsRoutes(app, deps) {
|
|
|
23
25
|
});
|
|
24
26
|
});
|
|
25
27
|
// 远程吊销某个会话(不影响当前 session)
|
|
26
|
-
app.post('/api/auth/sessions/:id/revoke', (req, res) => {
|
|
28
|
+
app.post('/api/auth/sessions/:id/revoke', async (req, res) => {
|
|
27
29
|
const user = auth(req, res);
|
|
28
30
|
if (!user)
|
|
29
31
|
return;
|
|
30
32
|
const sid = req.params.id;
|
|
31
|
-
const row =
|
|
33
|
+
const row = await dbOne("SELECT user_id, api_key FROM user_sessions WHERE id = ?", [sid]);
|
|
32
34
|
if (!row || row.user_id !== user.id)
|
|
33
35
|
return void res.status(404).json({ error: '会话不存在' });
|
|
34
36
|
const currentKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
|
|
35
37
|
if (row.api_key === currentKey)
|
|
36
38
|
return void res.json({ error: '不能吊销当前会话,请改用「全部登出」' });
|
|
37
|
-
|
|
39
|
+
await dbRun("UPDATE user_sessions SET revoked_at = datetime('now') WHERE id = ?", [sid]);
|
|
38
40
|
res.json({ ok: true });
|
|
39
41
|
});
|
|
40
42
|
// 一键全登出:rotate users.api_key + 吊销所有 session
|
|
41
43
|
// 要求密码二次验证(防 api_key 被盗后攻击者锁死真用户)
|
|
42
|
-
app.post('/api/auth/logout-all', (req, res) => {
|
|
44
|
+
app.post('/api/auth/logout-all', async (req, res) => {
|
|
43
45
|
const user = auth(req, res);
|
|
44
46
|
if (!user)
|
|
45
47
|
return;
|
|
@@ -49,9 +51,8 @@ export function registerAuthSessionsRoutes(app, deps) {
|
|
|
49
51
|
if (!verifyPassword(pwd, user.password_hash))
|
|
50
52
|
return void res.json({ error: '密码错误' });
|
|
51
53
|
const newKey = generateSecureKey('key');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.run(user.id);
|
|
54
|
+
await dbRun("UPDATE users SET api_key = ? WHERE id = ?", [newKey, user.id]);
|
|
55
|
+
await dbRun("UPDATE user_sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL", [user.id]);
|
|
55
56
|
// 为新 key 建一个 session 行(让当前发起者继续可用)
|
|
56
57
|
try {
|
|
57
58
|
recordSession(user.id, newKey, req);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerBlocklistRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
5
|
+
app.post('/api/blocklist/:user_id', async (req, res) => {
|
|
4
6
|
const me = auth(req, res);
|
|
5
7
|
if (!me)
|
|
6
8
|
return;
|
|
@@ -9,52 +11,51 @@ export function registerBlocklistRoutes(app, deps) {
|
|
|
9
11
|
return void res.json({ error: '不能拉黑自己' });
|
|
10
12
|
if (target === 'sys_protocol')
|
|
11
13
|
return void res.json({ error: '不能拉黑系统账户' });
|
|
12
|
-
const exists =
|
|
14
|
+
const exists = await dbOne("SELECT 1 FROM users WHERE id = ?", [target]);
|
|
13
15
|
if (!exists)
|
|
14
16
|
return void res.json({ error: '用户不存在' });
|
|
15
17
|
const reason = (req.body?.reason || '').toString().slice(0, 200);
|
|
16
|
-
|
|
17
|
-
.run(me.id, target, reason || null);
|
|
18
|
+
await dbRun("INSERT OR IGNORE INTO user_blocklist (blocker_id, blocked_id, reason) VALUES (?, ?, ?)", [me.id, target, reason || null]);
|
|
18
19
|
res.json({ ok: true });
|
|
19
20
|
});
|
|
20
|
-
app.delete('/api/blocklist/:user_id', (req, res) => {
|
|
21
|
+
app.delete('/api/blocklist/:user_id', async (req, res) => {
|
|
21
22
|
const me = auth(req, res);
|
|
22
23
|
if (!me)
|
|
23
24
|
return;
|
|
24
|
-
|
|
25
|
+
await dbRun("DELETE FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?", [me.id, req.params.user_id]);
|
|
25
26
|
res.json({ ok: true });
|
|
26
27
|
});
|
|
27
28
|
// D-2: 列表
|
|
28
|
-
app.get('/api/blocklist', (req, res) => {
|
|
29
|
+
app.get('/api/blocklist', async (req, res) => {
|
|
29
30
|
const me = auth(req, res);
|
|
30
31
|
if (!me)
|
|
31
32
|
return;
|
|
32
|
-
const rows =
|
|
33
|
+
const rows = await dbAll(`
|
|
33
34
|
SELECT b.blocked_id, b.reason, b.created_at,
|
|
34
35
|
u.name, u.handle, u.role
|
|
35
36
|
FROM user_blocklist b
|
|
36
37
|
JOIN users u ON u.id = b.blocked_id
|
|
37
38
|
WHERE b.blocker_id = ?
|
|
38
39
|
ORDER BY b.created_at DESC LIMIT 200
|
|
39
|
-
|
|
40
|
+
`, [me.id]);
|
|
40
41
|
res.json({ items: rows });
|
|
41
42
|
});
|
|
42
|
-
app.get('/api/blocklist/me', (req, res) => {
|
|
43
|
+
app.get('/api/blocklist/me', async (req, res) => {
|
|
43
44
|
const me = auth(req, res);
|
|
44
45
|
if (!me)
|
|
45
46
|
return;
|
|
46
|
-
const rows =
|
|
47
|
+
const rows = await dbAll(`
|
|
47
48
|
SELECT b.blocked_id, b.reason, b.created_at, u.name as blocked_name, u.role as blocked_role
|
|
48
49
|
FROM user_blocklist b LEFT JOIN users u ON u.id = b.blocked_id
|
|
49
50
|
WHERE b.blocker_id = ? ORDER BY b.created_at DESC
|
|
50
|
-
|
|
51
|
+
`, [me.id]);
|
|
51
52
|
res.json({ blocked: rows });
|
|
52
53
|
});
|
|
53
|
-
app.get('/api/blocklist/:user_id/status', (req, res) => {
|
|
54
|
+
app.get('/api/blocklist/:user_id/status', async (req, res) => {
|
|
54
55
|
const me = auth(req, res);
|
|
55
56
|
if (!me)
|
|
56
57
|
return;
|
|
57
|
-
const row =
|
|
58
|
+
const row = await dbOne("SELECT 1 FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?", [me.id, req.params.user_id]);
|
|
58
59
|
res.json({ blocked: !!row });
|
|
59
60
|
});
|
|
60
61
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
import { submitBuildFeedback, listMyBuildFeedback, getBuildFeedback, adminListBuildFeedback, adminUpdateBuildFeedback, triagePendingBuildFeedback, } from '../../layer2-business/L2-8-feedback/build-feedback-engine.js';
|
|
2
3
|
export function registerBuildFeedbackRoutes(app, deps) {
|
|
3
4
|
const { db, auth, requireSupportAdmin } = deps;
|
|
4
|
-
const hasPasskey = (userId) => ((
|
|
5
|
+
const hasPasskey = async (userId) => (((await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [userId]))?.n) || 0) > 0;
|
|
5
6
|
// ── 提交 ──────────────────────────────────────────────
|
|
6
|
-
app.post('/api/build-feedback', (req, res) => {
|
|
7
|
+
app.post('/api/build-feedback', async (req, res) => {
|
|
7
8
|
const user = auth(req, res);
|
|
8
9
|
if (!user)
|
|
9
10
|
return;
|
|
@@ -11,7 +12,7 @@ export function registerBuildFeedbackRoutes(app, deps) {
|
|
|
11
12
|
// 分级门(RFC-004 精确化 2026-06-05):
|
|
12
13
|
// 「报告问题」(ux_issue/bug = 用)→ 登录即可,不要 Passkey(没 Passkey 也要能报问题)
|
|
13
14
|
// 「建设平台」(proposal = 建)→ 必须 Passkey(真人锚点,后期贡献/奖励才有归属)
|
|
14
|
-
if (String(type) === 'proposal' && !hasPasskey(user.id)) {
|
|
15
|
+
if (String(type) === 'proposal' && !(await hasPasskey(user.id))) {
|
|
15
16
|
return void res.status(403).json({
|
|
16
17
|
error: '提交「改进提案 / proposal」需先绑定 Passkey 成为可问责真人 —— 提案是建设行为,被采纳会记入共建信誉,需真人锚点。报告 bug / 体验问题无需 Passkey。请在 webaz.xyz「我的」绑定 Passkey。',
|
|
17
18
|
error_code: 'PROPOSAL_REQUIRES_PASSKEY',
|
|
@@ -29,28 +30,28 @@ export function registerBuildFeedbackRoutes(app, deps) {
|
|
|
29
30
|
res.json(result);
|
|
30
31
|
});
|
|
31
32
|
// ── 闭环:我的反馈进度 ──(必须在 /:id 之前声明)──────────
|
|
32
|
-
app.get('/api/build-feedback/mine', (req, res) => {
|
|
33
|
+
app.get('/api/build-feedback/mine', async (req, res) => {
|
|
33
34
|
const user = auth(req, res);
|
|
34
35
|
if (!user)
|
|
35
36
|
return;
|
|
36
|
-
res.json({ feedback: listMyBuildFeedback(db, user.id) });
|
|
37
|
+
res.json({ feedback: await listMyBuildFeedback(db, user.id) });
|
|
37
38
|
});
|
|
38
|
-
app.get('/api/build-feedback/:id', (req, res) => {
|
|
39
|
+
app.get('/api/build-feedback/:id', async (req, res) => {
|
|
39
40
|
const user = auth(req, res);
|
|
40
41
|
if (!user)
|
|
41
42
|
return;
|
|
42
43
|
const isAdmin = !!(user.is_admin || user.admin_permissions); // 宽松判定;admin 端点另有严格门
|
|
43
|
-
const row = getBuildFeedback(db, String(req.params.id), user.id, !!isAdmin);
|
|
44
|
+
const row = await getBuildFeedback(db, String(req.params.id), user.id, !!isAdmin);
|
|
44
45
|
if (!row)
|
|
45
46
|
return void res.status(404).json({ error: '反馈不存在或无权查看' });
|
|
46
47
|
res.json(row);
|
|
47
48
|
});
|
|
48
49
|
// ── maintainer triage ────────────────────────────────
|
|
49
|
-
app.get('/api/admin/build-feedback', (req, res) => {
|
|
50
|
+
app.get('/api/admin/build-feedback', async (req, res) => {
|
|
50
51
|
if (!requireSupportAdmin(req, res))
|
|
51
52
|
return;
|
|
52
53
|
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
53
|
-
res.json({ feedback: adminListBuildFeedback(db, status) });
|
|
54
|
+
res.json({ feedback: await adminListBuildFeedback(db, status) });
|
|
54
55
|
});
|
|
55
56
|
// RFC-005 Phase 2:AI 自动 triage(advisory)— 批量处理 received 反馈:去重 + 标风险/摘要 + 置 triaged。
|
|
56
57
|
// 不 resolve、不记功(人类的)。无 AI key 时只做确定性去重 + 置 triaged。
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { getBuildProfile } from '../../layer2-business/L2-9-contribution/build-reputation-engine.js';
|
|
2
|
+
import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
|
|
2
3
|
export function registerBuildReputationRoutes(app, deps) {
|
|
3
4
|
const { db, auth } = deps;
|
|
4
|
-
|
|
5
|
+
// PR-5A/5B: this legacy RFC-006 contributor dashboard is a contribution display surface, so its
|
|
6
|
+
// response is wrapped in the uncommitted-value boundary (RFC-017 I-12 / §7) — build_points/tier express
|
|
7
|
+
// BUILD reputation (coordination layer) only and promise no economic value.
|
|
8
|
+
app.get('/api/build-reputation/me', async (req, res) => {
|
|
5
9
|
const user = auth(req, res);
|
|
6
10
|
if (!user)
|
|
7
11
|
return;
|
|
8
|
-
res.json(getBuildProfile(db, user.id));
|
|
12
|
+
res.json(withUncommittedValueBoundary(await getBuildProfile(db, user.id)));
|
|
9
13
|
});
|
|
10
14
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { createBuildTask,
|
|
1
|
+
import { createBuildTask, claimBuildTask, submitBuildTask, releaseBuildTask, resolveBuildTask, } from '../../layer2-business/L2-9-contribution/build-tasks-engine.js';
|
|
2
|
+
// PR9C-1 — read/filter the build_tasks core + PR9B agent metadata (member scope; restricted/internal hidden).
|
|
3
|
+
import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
|
|
4
|
+
// PR9C-2 — participation guard (claim/submit/release): restricted/internal 404 no-leak, auto_claimable, canonical PR.
|
|
5
|
+
import { guardParticipation, validatePrRefAgainstCanonical } from '../../layer2-business/L2-9-contribution/build-task-participation.js';
|
|
2
6
|
export function registerBuildTasksRoutes(app, deps) {
|
|
3
7
|
const { db, auth, requireSupportAdmin } = deps;
|
|
4
8
|
app.post('/api/build-tasks', (req, res) => {
|
|
@@ -11,53 +15,81 @@ export function registerBuildTasksRoutes(app, deps) {
|
|
|
11
15
|
return void res.status(result.error_code === 'RATE_LIMITED' ? 429 : 400).json(result);
|
|
12
16
|
res.json(result);
|
|
13
17
|
});
|
|
18
|
+
// PR9C-1: list now returns build_tasks core + parsed agent_metadata (null for old tasks) under the
|
|
19
|
+
// uncommitted value_boundary; member scope hides restricted/internal. Bad filter → fail-closed 400.
|
|
14
20
|
app.get('/api/build-tasks', (req, res) => {
|
|
15
21
|
const user = auth(req, res);
|
|
16
22
|
if (!user)
|
|
17
23
|
return;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const v = validateTaskFilters(req.query);
|
|
25
|
+
if (!v.ok)
|
|
26
|
+
return void res.status(400).json({ error: v.detail, error_code: v.code });
|
|
27
|
+
if (req.query.mine === '1')
|
|
28
|
+
v.filters.claimerId = user.id;
|
|
29
|
+
const tasks = listBuildTasksWithAgentMetadata(db, v.filters, 'member');
|
|
30
|
+
res.json(withContributionReadEnvelope({ tasks }));
|
|
22
31
|
});
|
|
23
32
|
app.get('/api/build-tasks/:id', (req, res) => {
|
|
24
33
|
const user = auth(req, res);
|
|
25
34
|
if (!user)
|
|
26
35
|
return;
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
36
|
+
const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'member'); // null → not found OR restricted/internal (no leak)
|
|
37
|
+
if (!task)
|
|
29
38
|
return void res.status(404).json({ error: '任务不存在' });
|
|
30
|
-
|
|
39
|
+
// backward-compat: spread the legacy build_tasks fields + events at top level; only append the new fields.
|
|
40
|
+
res.json(withContributionReadEnvelope(task));
|
|
31
41
|
});
|
|
42
|
+
// PR9C-2: participation guard runs BEFORE the engine — restricted/internal → 404 no-leak; metadata public
|
|
43
|
+
// task → claim respects auto_claimable. Success appends value_boundary + canonical_contribution_target only.
|
|
32
44
|
app.post('/api/build-tasks/:id/claim', (req, res) => {
|
|
33
45
|
const user = auth(req, res);
|
|
34
46
|
if (!user)
|
|
35
47
|
return;
|
|
48
|
+
const g = guardParticipation(db, String(req.params.id), 'claim');
|
|
49
|
+
if (!g.ok)
|
|
50
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
36
51
|
const result = claimBuildTask(db, String(req.params.id), user.id, (req.body ?? {}).provenance);
|
|
37
52
|
if ('error' in result) {
|
|
38
53
|
const code = result.error_code === 'NOT_FOUND' ? 404 : result.error_code === 'TOO_MANY_CLAIMS' ? 429 : 409;
|
|
39
54
|
return void res.status(code).json(result);
|
|
40
55
|
}
|
|
41
|
-
res.json(result);
|
|
56
|
+
res.json(withContributionReadEnvelope(result));
|
|
42
57
|
});
|
|
43
58
|
app.post('/api/build-tasks/:id/submit', (req, res) => {
|
|
44
59
|
const user = auth(req, res);
|
|
45
60
|
if (!user)
|
|
46
61
|
return;
|
|
47
|
-
const
|
|
48
|
-
|
|
62
|
+
const g = guardParticipation(db, String(req.params.id), 'submit');
|
|
63
|
+
if (!g.ok)
|
|
64
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
65
|
+
const { pr_ref, note, verification_summary } = req.body ?? {};
|
|
66
|
+
// anti GitHub-target confusion: a PR must target the canonical repo (the response shows where to submit).
|
|
67
|
+
const pr = validatePrRefAgainstCanonical(pr_ref);
|
|
68
|
+
if (!pr.ok)
|
|
69
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: pr.message, error_code: pr.code }));
|
|
70
|
+
// submit evidence (design contract): a PR/ref alone is not enough — the contributor must summarize what
|
|
71
|
+
// they ran/verified (the task's verification_commands + results). Fail-closed; stored in the event log.
|
|
72
|
+
const vs = typeof verification_summary === 'string' ? verification_summary.trim() : '';
|
|
73
|
+
if (vs.length < 1)
|
|
74
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: 'submit requires a verification_summary — summarize what you ran/verified (the task verification_commands and their results)', error_code: 'VERIFICATION_SUMMARY_REQUIRED' }));
|
|
75
|
+
if (vs.length > 2000)
|
|
76
|
+
return void res.status(400).json(withContributionReadEnvelope({ error: 'verification_summary too long (max 2000 chars)', error_code: 'VERIFICATION_SUMMARY_TOO_LONG' }));
|
|
77
|
+
const result = submitBuildTask(db, String(req.params.id), user.id, pr_ref, note, vs);
|
|
49
78
|
if ('error' in result)
|
|
50
79
|
return void res.status(result.error_code === 'NOT_FOUND' ? 404 : 400).json(result);
|
|
51
|
-
res.json(result);
|
|
80
|
+
res.json(withContributionReadEnvelope(result));
|
|
52
81
|
});
|
|
53
82
|
app.post('/api/build-tasks/:id/release', (req, res) => {
|
|
54
83
|
const user = auth(req, res);
|
|
55
84
|
if (!user)
|
|
56
85
|
return;
|
|
86
|
+
const g = guardParticipation(db, String(req.params.id), 'release');
|
|
87
|
+
if (!g.ok)
|
|
88
|
+
return void res.status(g.status).json({ error: g.message, error_code: g.code });
|
|
57
89
|
const result = releaseBuildTask(db, String(req.params.id), user.id);
|
|
58
90
|
if ('error' in result)
|
|
59
91
|
return void res.status(result.error_code === 'NOT_FOUND' ? 404 : 400).json(result);
|
|
60
|
-
res.json(result);
|
|
92
|
+
res.json(withContributionReadEnvelope(result));
|
|
61
93
|
});
|
|
62
94
|
// 验收终态 —— 仅 admin/maintainer(验收=真人,RFC-006 不变量 2;不发奖励/不记信誉)
|
|
63
95
|
app.post('/api/admin/build-tasks/:id/resolve', (req, res) => {
|