@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.
Files changed (195) hide show
  1. package/README.md +5 -1
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  38. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  39. package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
  40. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
  41. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  42. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  43. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  44. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  45. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  46. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  47. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  48. package/dist/pwa/acp-feed.js +13 -1
  49. package/dist/pwa/admin-bearer-auth.js +21 -0
  50. package/dist/pwa/contract-fingerprint.js +2 -0
  51. package/dist/pwa/email-delivery.js +127 -0
  52. package/dist/pwa/endpoint-actions.js +5 -1
  53. package/dist/pwa/goal-index.js +8 -8
  54. package/dist/pwa/human-presence.js +62 -0
  55. package/dist/pwa/public/app.js +1485 -283
  56. package/dist/pwa/public/i18n.js +297 -59
  57. package/dist/pwa/public/index.html +1 -0
  58. package/dist/pwa/public/openapi.json +5 -5
  59. package/dist/pwa/public/whitepaper/en/index.html +153 -0
  60. package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
  61. package/dist/pwa/rate-limit.js +22 -0
  62. package/dist/pwa/routes/account-deletion.js +15 -13
  63. package/dist/pwa/routes/addresses.js +10 -9
  64. package/dist/pwa/routes/admin-admins.js +13 -14
  65. package/dist/pwa/routes/admin-analytics.js +109 -69
  66. package/dist/pwa/routes/admin-atomic.js +10 -4
  67. package/dist/pwa/routes/admin-catalog.js +13 -11
  68. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  69. package/dist/pwa/routes/admin-events.js +5 -3
  70. package/dist/pwa/routes/admin-health.js +2 -1
  71. package/dist/pwa/routes/admin-moderation.js +50 -29
  72. package/dist/pwa/routes/admin-ops.js +35 -23
  73. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  74. package/dist/pwa/routes/admin-reports.js +23 -21
  75. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  76. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  77. package/dist/pwa/routes/admin-users-query.js +65 -53
  78. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  79. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  80. package/dist/pwa/routes/admin-wallet-ops.js +32 -7
  81. package/dist/pwa/routes/agent-buy.js +46 -22
  82. package/dist/pwa/routes/agent-governance.js +52 -56
  83. package/dist/pwa/routes/ai.js +7 -5
  84. package/dist/pwa/routes/analytics.js +43 -41
  85. package/dist/pwa/routes/anchors.js +19 -20
  86. package/dist/pwa/routes/announcements.js +13 -13
  87. package/dist/pwa/routes/arbitrator.js +97 -31
  88. package/dist/pwa/routes/auction.js +157 -116
  89. package/dist/pwa/routes/auth-login.js +6 -4
  90. package/dist/pwa/routes/auth-read.js +21 -10
  91. package/dist/pwa/routes/auth-register.js +111 -26
  92. package/dist/pwa/routes/auth-sessions.js +12 -11
  93. package/dist/pwa/routes/blocklist.js +16 -15
  94. package/dist/pwa/routes/build-feedback.js +10 -9
  95. package/dist/pwa/routes/build-reputation.js +6 -2
  96. package/dist/pwa/routes/build-tasks.js +45 -13
  97. package/dist/pwa/routes/buyer-feeds.js +27 -25
  98. package/dist/pwa/routes/cart.js +16 -15
  99. package/dist/pwa/routes/charity.js +212 -150
  100. package/dist/pwa/routes/chat.js +42 -43
  101. package/dist/pwa/routes/checkin-tasks.js +10 -9
  102. package/dist/pwa/routes/checkout-helpers.js +12 -10
  103. package/dist/pwa/routes/claim-initiators.js +34 -14
  104. package/dist/pwa/routes/claim-verify.js +86 -53
  105. package/dist/pwa/routes/claim-voting.js +43 -18
  106. package/dist/pwa/routes/contribution-identity.js +164 -0
  107. package/dist/pwa/routes/contribution-score.js +19 -0
  108. package/dist/pwa/routes/coupons.js +19 -16
  109. package/dist/pwa/routes/dashboards.js +18 -16
  110. package/dist/pwa/routes/dispute-cases.js +25 -24
  111. package/dist/pwa/routes/disputes-read.js +45 -51
  112. package/dist/pwa/routes/disputes-write.js +124 -61
  113. package/dist/pwa/routes/evidence.js +9 -9
  114. package/dist/pwa/routes/external-anchors.js +13 -12
  115. package/dist/pwa/routes/feedback.js +29 -33
  116. package/dist/pwa/routes/flash-sales.js +18 -16
  117. package/dist/pwa/routes/follows.js +25 -24
  118. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  119. package/dist/pwa/routes/governance-onboarding.js +70 -59
  120. package/dist/pwa/routes/group-buys.js +22 -22
  121. package/dist/pwa/routes/growth.js +34 -31
  122. package/dist/pwa/routes/import-product.js +12 -10
  123. package/dist/pwa/routes/kyc.js +9 -8
  124. package/dist/pwa/routes/leaderboard.js +20 -18
  125. package/dist/pwa/routes/listings.js +23 -22
  126. package/dist/pwa/routes/logistics.js +10 -8
  127. package/dist/pwa/routes/manifests.js +27 -27
  128. package/dist/pwa/routes/me-data.js +23 -21
  129. package/dist/pwa/routes/notifications.js +7 -6
  130. package/dist/pwa/routes/offers.js +30 -12
  131. package/dist/pwa/routes/orders-action.js +51 -29
  132. package/dist/pwa/routes/orders-create.js +75 -20
  133. package/dist/pwa/routes/orders-read.js +21 -20
  134. package/dist/pwa/routes/p2p-products.js +30 -18
  135. package/dist/pwa/routes/payments-governance.js +61 -56
  136. package/dist/pwa/routes/peers.js +9 -8
  137. package/dist/pwa/routes/pin-receipts.js +13 -13
  138. package/dist/pwa/routes/products-aliases.js +12 -10
  139. package/dist/pwa/routes/products-claims.js +36 -17
  140. package/dist/pwa/routes/products-create.js +53 -38
  141. package/dist/pwa/routes/products-crud.js +17 -16
  142. package/dist/pwa/routes/products-links.js +49 -26
  143. package/dist/pwa/routes/products-list.js +6 -4
  144. package/dist/pwa/routes/products-meta.js +40 -39
  145. package/dist/pwa/routes/products-update.js +19 -5
  146. package/dist/pwa/routes/profile-credentials.js +20 -19
  147. package/dist/pwa/routes/profile-identity.js +14 -13
  148. package/dist/pwa/routes/profile-location.js +7 -6
  149. package/dist/pwa/routes/profile-placement.js +20 -19
  150. package/dist/pwa/routes/profile-prefs.js +11 -11
  151. package/dist/pwa/routes/promoter.js +58 -66
  152. package/dist/pwa/routes/public-build-tasks.js +19 -0
  153. package/dist/pwa/routes/public-utils.js +108 -46
  154. package/dist/pwa/routes/push.js +16 -15
  155. package/dist/pwa/routes/ratings.js +92 -32
  156. package/dist/pwa/routes/recover-key.js +66 -26
  157. package/dist/pwa/routes/referral.js +37 -52
  158. package/dist/pwa/routes/reputation.js +3 -2
  159. package/dist/pwa/routes/returns.js +76 -73
  160. package/dist/pwa/routes/reviews.js +41 -18
  161. package/dist/pwa/routes/rewards-apply.js +16 -15
  162. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  163. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  164. package/dist/pwa/routes/rfqs.js +163 -85
  165. package/dist/pwa/routes/search.js +16 -14
  166. package/dist/pwa/routes/secondhand.js +25 -22
  167. package/dist/pwa/routes/seller-quota.js +24 -26
  168. package/dist/pwa/routes/share-redirects.js +60 -55
  169. package/dist/pwa/routes/shareables-interactions.js +34 -35
  170. package/dist/pwa/routes/shareables.js +55 -51
  171. package/dist/pwa/routes/shop-referral.js +58 -0
  172. package/dist/pwa/routes/shops.js +25 -20
  173. package/dist/pwa/routes/signaling.js +10 -9
  174. package/dist/pwa/routes/skill-market.js +16 -16
  175. package/dist/pwa/routes/skills.js +15 -14
  176. package/dist/pwa/routes/snf.js +14 -13
  177. package/dist/pwa/routes/tags.js +10 -9
  178. package/dist/pwa/routes/task-proposals.js +121 -0
  179. package/dist/pwa/routes/trial.js +72 -52
  180. package/dist/pwa/routes/trusted-kpi.js +20 -18
  181. package/dist/pwa/routes/url-claim.js +67 -28
  182. package/dist/pwa/routes/users-public.js +62 -70
  183. package/dist/pwa/routes/variants.js +12 -13
  184. package/dist/pwa/routes/verifier-user.js +61 -21
  185. package/dist/pwa/routes/verify-tasks.js +49 -25
  186. package/dist/pwa/routes/waitlist.js +16 -15
  187. package/dist/pwa/routes/wallet-read.js +75 -37
  188. package/dist/pwa/routes/wallet-write.js +12 -9
  189. package/dist/pwa/routes/webauthn.js +25 -26
  190. package/dist/pwa/routes/webhooks.js +26 -26
  191. package/dist/pwa/routes/welcome.js +45 -50
  192. package/dist/pwa/routes/wishlist-qa.js +29 -32
  193. package/dist/pwa/server.js +304 -90
  194. package/dist/version.js +1 -1
  195. 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, resolveUserRef, generateId, generateSecureKey, generatePermanentCode, deriveHandle, clientIpHash, clientUaHash, pickPreferredSide, joinPowerLeg, inviteRotationLookup, recordSession, broadcastSystemEvent } = deps;
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, placement_side, turnstile_token } = req.body;
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 = db.prepare("SELECT 1 FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?) LIMIT 1").get(trimmed, INTERNAL_AUDITOR_ID);
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 = db.prepare("SELECT value FROM system_state WHERE key='require_ref_to_register'").get()?.value === '1';
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
- let sponsorRawRef = (sponsor_id && typeof sponsor_id === 'string') ? sponsor_id.trim() : '';
57
- let suffixSide = null;
58
- const sufM = sponsorRawRef.match(/^(.+?)-([lLrR])$/);
59
- if (sufM) {
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 resolvedSponsorId = resolveUserRef(sponsorRawRef);
65
- if (!resolvedSponsorId || resolvedSponsorId === 'sys_protocol' || resolvedSponsorId === INTERNAL_AUDITOR_ID) {
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
- const sponsor = db.prepare("SELECT id, sponsor_path FROM users WHERE id = ?")
69
- .get(resolvedSponsorId);
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 = db.prepare(`SELECT COUNT(*) AS n FROM registration_audit_log WHERE ip_hash = ? AND created_at > datetime('now','-1 hour')`).get(ipHash).n;
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
- let effectiveInviter = placement_inviter_id ? resolveUserRef(String(placement_inviter_id).replace(/-[lLrR]$/, '')) : null;
105
- let effectiveSide = (placement_side === 'left' || placement_side === 'right') ? placement_side : suffixSide;
106
- if (!effectiveInviter && sponsorRawRef)
107
- effectiveInviter = resolveUserRef(sponsorRawRef);
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
- console.error('[register-tx]', e.message);
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
- const { db, auth, verifyPassword, recordSession, generateSecureKey } = deps;
3
- app.get('/api/auth/sessions', (req, res) => {
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 = db.prepare(`
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
- `).all(user.id);
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 = db.prepare("SELECT user_id, api_key FROM user_sessions WHERE id = ?").get(sid);
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
- db.prepare("UPDATE user_sessions SET revoked_at = datetime('now') WHERE id = ?").run(sid);
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
- db.prepare("UPDATE users SET api_key = ? WHERE id = ?").run(newKey, user.id);
53
- db.prepare("UPDATE user_sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL")
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
- const { db, auth } = deps;
3
- app.post('/api/blocklist/:user_id', (req, res) => {
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 = db.prepare("SELECT 1 FROM users WHERE id = ?").get(target);
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
- db.prepare("INSERT OR IGNORE INTO user_blocklist (blocker_id, blocked_id, reason) VALUES (?, ?, ?)")
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
- db.prepare("DELETE FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?").run(me.id, req.params.user_id);
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 = db.prepare(`
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
- `).all(me.id);
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 = db.prepare(`
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
- `).all(me.id);
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 = db.prepare("SELECT 1 FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?").get(me.id, req.params.user_id);
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) => ((db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?').get(userId)?.n) || 0) > 0;
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
- app.get('/api/build-reputation/me', (req, res) => {
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, listBuildTasks, getBuildTask, claimBuildTask, submitBuildTask, releaseBuildTask, resolveBuildTask, } from '../../layer2-business/L2-9-contribution/build-tasks-engine.js';
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 status = typeof req.query.status === 'string' ? req.query.status : undefined;
19
- const area = typeof req.query.area === 'string' ? req.query.area : undefined;
20
- const claimerId = req.query.mine === '1' ? user.id : undefined;
21
- res.json({ tasks: listBuildTasks(db, { status, area, claimerId }) });
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 row = getBuildTask(db, String(req.params.id));
28
- if (!row)
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
- res.json(row);
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 { pr_ref, note } = req.body ?? {};
48
- const result = submitBuildTask(db, String(req.params.id), user.id, pr_ref, note);
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) => {