@seasonkoh/webaz 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +2 -0
  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 +165 -64
  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 +173 -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 +10 -2
  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-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -1,3 +1,12 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
2
+ // RFC-016 Phase 1 — 仅端点纯校验读/列表/公开查询/读回 + 单语句标记/字段写 + 写后通知 → async seam。
3
+ // 全部保持同步(Phase 3 再用 pg tx/行锁):
4
+ // - 模块级 helper(settleClaimTask 三路径结算 / distributePool / checkAndApplyOutlierStrike /
5
+ // notifyEligibleVerifiers / isEligibleClaimVerifier / activeClaimTaskCountForVerifier /
6
+ // processClaimTaskQueue)——settleClaimTask 是裸(非 db.transaction)多写结算序列,
7
+ // 由 vote 端点与 cron 调用,必须整体同步;
8
+ // - claim 发起锁押序列(INSERT task + 锁 stake + has_pending_claim);
9
+ // - vote 共识序列(票数 guard + INSERT vote + 收齐重数 + seal),seal 后同步调 settleClaimTask。
1
10
  // ─── 域常量 ───────────────────────────────────────────────
2
11
  export const CLAIM_STAKE_DEFAULT = 10; // 买家发起质押 10 WAZ
3
12
  export const CLAIM_DEADLINE_HOURS = 48; // 接单 + 投票截止
@@ -279,11 +288,11 @@ export function processClaimTaskQueue(db, generateId) {
279
288
  export function registerClaimVerifyRoutes(app, deps) {
280
289
  const { db, auth, generateId, requireHumanPresence } = deps;
281
290
  // 买家发起 claim 验证任务(绑定 paid 及之后的订单)
282
- app.post('/api/orders/:id/claim-verification', (req, res) => {
291
+ app.post('/api/orders/:id/claim-verification', async (req, res) => {
283
292
  const user = auth(req, res);
284
293
  if (!user)
285
294
  return;
286
- const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id);
295
+ const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.id]);
287
296
  if (!order)
288
297
  return void res.status(404).json({ error: '订单不存在' });
289
298
  if (order.buyer_id !== user.id)
@@ -292,7 +301,7 @@ export function registerClaimVerifyRoutes(app, deps) {
292
301
  if (blockedStatuses.has(order.status)) {
293
302
  return void res.status(400).json({ error: `当前订单状态(${order.status})不可发起验证` });
294
303
  }
295
- const existing = db.prepare('SELECT id FROM claim_verification_tasks WHERE order_id = ?').get(req.params.id);
304
+ const existing = await dbOne('SELECT id FROM claim_verification_tasks WHERE order_id = ?', [req.params.id]);
296
305
  if (existing)
297
306
  return void res.status(409).json({ error: '该订单已存在验证任务(不可撤销)', task_id: existing.id });
298
307
  const claim_target = String(req.body?.claim_target || '').trim();
@@ -304,7 +313,7 @@ export function registerClaimVerifyRoutes(app, deps) {
304
313
  return void res.status(400).json({ error: 'claim_text 长度需 6-500 字' });
305
314
  }
306
315
  const evidence_uri = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
307
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
316
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
308
317
  const stake = CLAIM_STAKE_DEFAULT;
309
318
  if (!wallet || wallet.balance < stake) {
310
319
  return void res.status(400).json({ error: `余额不足:发起需锁 ${stake} WAZ,当前余额 ${wallet?.balance ?? 0} WAZ` });
@@ -312,17 +321,43 @@ export function registerClaimVerifyRoutes(app, deps) {
312
321
  const id = generateId('cvt');
313
322
  const deadline = new Date(Date.now() + CLAIM_DEADLINE_HOURS * 3600_000).toISOString();
314
323
  const sellerId = order.seller_id;
315
- db.prepare(`INSERT INTO claim_verification_tasks
316
- (id, order_id, buyer_id, seller_id, product_id, claim_target, claim_text, evidence_uri, stake_buyer, deadline_at, status)
317
- VALUES (?,?,?,?,?,?,?,?,?,?, 'open')`).run(id, req.params.id, user.id, sellerId, order.product_id, claim_target, claim_text, evidence_uri, stake, deadline);
318
- db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
319
- .run(stake, stake, user.id);
320
- db.prepare('UPDATE orders SET has_pending_claim = 1 WHERE id = ?').run(req.params.id);
321
- const productTitle = db.prepare('SELECT title FROM products WHERE id = ?').get(order.product_id)?.title || '—';
324
+ // Codex #237 P1:原为裸多写序列(await 预检后直接 3 连写,无 db.transaction、无余额守卫)。
325
+ // 包进 db.transaction + tx 内重检无重复 task + 余额守卫扣押 + 订单 flag CAS;任一失败回滚全部。
326
+ const BLOCKED = ['created', 'cancelled', 'completed', 'refunded'];
327
+ try {
328
+ db.transaction(() => {
329
+ const dup = db.prepare('SELECT id FROM claim_verification_tasks WHERE order_id = ?').get(req.params.id);
330
+ if (dup)
331
+ throw new Error('CLAIM_EXISTS');
332
+ db.prepare(`INSERT INTO claim_verification_tasks
333
+ (id, order_id, buyer_id, seller_id, product_id, claim_target, claim_text, evidence_uri, stake_buyer, deadline_at, status)
334
+ VALUES (?,?,?,?,?,?,?,?,?,?, 'open')`).run(id, req.params.id, user.id, sellerId, order.product_id, claim_target, claim_text, evidence_uri, stake, deadline);
335
+ const d = db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ? AND balance >= ?')
336
+ .run(stake, stake, user.id, stake);
337
+ if (d.changes !== 1)
338
+ throw new Error('CLAIM_INSUFFICIENT_BALANCE');
339
+ const o = db.prepare(`UPDATE orders SET has_pending_claim = 1 WHERE id = ? AND (has_pending_claim IS NULL OR has_pending_claim != 1) AND status NOT IN ('created','cancelled','completed','refunded')`).run(req.params.id);
340
+ if (o.changes !== 1)
341
+ throw new Error('CLAIM_ORDER_BLOCKED');
342
+ })();
343
+ }
344
+ catch (e) {
345
+ const m = e.message;
346
+ if (m === 'CLAIM_EXISTS')
347
+ return void res.status(409).json({ error: '该订单已存在验证任务(不可撤销)' });
348
+ if (m === 'CLAIM_INSUFFICIENT_BALANCE')
349
+ return void res.status(400).json({ error: `余额不足:发起需锁 ${stake} WAZ` });
350
+ if (m === 'CLAIM_ORDER_BLOCKED')
351
+ return void res.status(400).json({ error: `当前订单状态不可发起验证(${BLOCKED.join('/')} 或已挂验证)` });
352
+ throw e;
353
+ }
354
+ const productTitle = (await dbOne('SELECT title FROM products WHERE id = ?', [order.product_id]))?.title || '—';
322
355
  const claimLabel = CLAIM_TARGET_LABEL_ZH[claim_target] || claim_target;
323
356
  try {
324
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`)
325
- .run(generateId('ntf'), sellerId, 'claim_new', `⚠️ 买家发起验证:${claimLabel}`, `订单「${productTitle}」 — 48h 内提交证据可延期至 verifier 共识结案`, req.params.id);
357
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), sellerId, 'claim_new',
358
+ `⚠️ 买家发起验证:${claimLabel}`,
359
+ `订单「${productTitle}」 — 48h 内提交证据可延期至 verifier 共识结案`,
360
+ req.params.id]);
326
361
  }
327
362
  catch (e) {
328
363
  console.error('[V2 notify seller]', e.message);
@@ -341,25 +376,23 @@ export function registerClaimVerifyRoutes(app, deps) {
341
376
  res.json({ success: true, task_id: id, deadline_at: deadline, stake_locked: stake });
342
377
  });
343
378
  // 通过 order_id 查关联 task
344
- app.get('/api/orders/:id/claim-task', (req, res) => {
379
+ app.get('/api/orders/:id/claim-task', async (req, res) => {
345
380
  const user = auth(req, res);
346
381
  if (!user)
347
382
  return;
348
- const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE order_id = ?')
349
- .get(req.params.id);
383
+ const task = await dbOne('SELECT * FROM claim_verification_tasks WHERE order_id = ?', [req.params.id]);
350
384
  if (!task)
351
385
  return void res.json({ task: null });
352
- const hasVoted = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
353
- .get(task.id, user.id);
386
+ const hasVoted = await dbOne('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?', [task.id, user.id]);
354
387
  const isParty = task.buyer_id === user.id || task.seller_id === user.id;
355
388
  const elig = isEligibleClaimVerifier(db, user.id);
356
389
  if (!isParty && !hasVoted && !elig.ok)
357
390
  return void res.json({ task: null, visibility: 'restricted' });
358
- const votes = db.prepare(`SELECT verifier_id, vote, voted_at FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`).all(task.id);
391
+ const votes = await dbAll(`SELECT verifier_id, vote, voted_at FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`, [task.id]);
359
392
  res.json({ task, votes, votes_needed: CLAIM_VERIFIERS_NEEDED });
360
393
  });
361
394
  // 列出可接的 open 任务
362
- app.get('/api/claim-tasks/available', (req, res) => {
395
+ app.get('/api/claim-tasks/available', async (req, res) => {
363
396
  const user = auth(req, res);
364
397
  if (!user)
365
398
  return;
@@ -370,7 +403,7 @@ export function registerClaimVerifyRoutes(app, deps) {
370
403
  if (active >= CLAIM_VERIFIER_MAX_ACTIVE) {
371
404
  return void res.status(429).json({ error: `已有 ${active} 个进行中任务(上限 ${CLAIM_VERIFIER_MAX_ACTIVE}),请先完成`, active });
372
405
  }
373
- const rows = db.prepare(`
406
+ const rows = await dbAll(`
374
407
  SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target, cvt.claim_text,
375
408
  cvt.evidence_uri, cvt.seller_evidence_uri, cvt.deadline_at, cvt.created_at,
376
409
  (SELECT COUNT(*) FROM claim_verification_votes WHERE task_id = cvt.id AND vote != 'abstain') as votes_count,
@@ -383,11 +416,11 @@ export function registerClaimVerifyRoutes(app, deps) {
383
416
  AND (SELECT COUNT(*) FROM claim_verification_votes WHERE task_id = cvt.id AND vote != 'abstain') < ?
384
417
  ORDER BY cvt.created_at ASC
385
418
  LIMIT 50
386
- `).all(user.id, user.id, user.id, CLAIM_VERIFIERS_NEEDED);
419
+ `, [user.id, user.id, user.id, CLAIM_VERIFIERS_NEEDED]);
387
420
  res.json({ eligible: true, via: elig.via, active, max_active: CLAIM_VERIFIER_MAX_ACTIVE, tasks: rows });
388
421
  });
389
422
  // verifier 投票 — 铁律 §4
390
- app.post('/api/claim-tasks/:id/vote', (req, res) => {
423
+ app.post('/api/claim-tasks/:id/vote', async (req, res) => {
391
424
  const user = auth(req, res);
392
425
  if (!user)
393
426
  return;
@@ -401,7 +434,7 @@ export function registerClaimVerifyRoutes(app, deps) {
401
434
  });
402
435
  if (!hpCheck.ok)
403
436
  return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
404
- const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?').get(req.params.id);
437
+ const task = await dbOne('SELECT * FROM claim_verification_tasks WHERE id = ?', [req.params.id]);
405
438
  if (!task)
406
439
  return void res.status(404).json({ error: '任务不存在' });
407
440
  if (task.status !== 'open')
@@ -416,8 +449,7 @@ export function registerClaimVerifyRoutes(app, deps) {
416
449
  }
417
450
  const evidence_uri = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
418
451
  const note = req.body?.note ? String(req.body.note).trim().slice(0, 500) : null;
419
- const dup = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
420
- .get(req.params.id, user.id);
452
+ const dup = await dbOne('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?', [req.params.id, user.id]);
421
453
  if (dup)
422
454
  return void res.status(409).json({ error: '已投过票' });
423
455
  const votesNow = db.prepare(`SELECT COUNT(*) as n FROM claim_verification_votes WHERE task_id = ? AND vote != 'abstain'`)
@@ -452,42 +484,42 @@ export function registerClaimVerifyRoutes(app, deps) {
452
484
  });
453
485
  });
454
486
  // 我相关的任务(必须在 /:id 之前注册,否则被 /:id 截获)
455
- app.get('/api/claim-tasks/mine', (req, res) => {
487
+ app.get('/api/claim-tasks/mine', async (req, res) => {
456
488
  const user = auth(req, res);
457
489
  if (!user)
458
490
  return;
459
- const asBuyer = db.prepare(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
460
- FROM claim_verification_tasks WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
461
- const asSeller = db.prepare(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
462
- FROM claim_verification_tasks WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
463
- const asVerifier = db.prepare(`
491
+ const asBuyer = await dbAll(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
492
+ FROM claim_verification_tasks WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 50`, [user.id]);
493
+ const asSeller = await dbAll(`SELECT id, order_id, product_id, claim_target, status, deadline_at, created_at
494
+ FROM claim_verification_tasks WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`, [user.id]);
495
+ const asVerifier = await dbAll(`
464
496
  SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target, cvt.status, cvt.deadline_at, cvt.created_at,
465
497
  cvv.vote, cvv.voted_at
466
498
  FROM claim_verification_votes cvv
467
499
  JOIN claim_verification_tasks cvt ON cvt.id = cvv.task_id
468
500
  WHERE cvv.verifier_id = ?
469
501
  ORDER BY cvv.voted_at DESC
470
- LIMIT 50`).all(user.id);
502
+ LIMIT 50`, [user.id]);
471
503
  res.json({ as_buyer: asBuyer, as_seller: asSeller, as_verifier: asVerifier });
472
504
  });
473
505
  // 通知偏好
474
- app.post('/api/me/notify-claim-tasks', (req, res) => {
506
+ app.post('/api/me/notify-claim-tasks', async (req, res) => {
475
507
  const user = auth(req, res);
476
508
  if (!user)
477
509
  return;
478
510
  const enabled = req.body?.enabled === false ? 0 : 1;
479
- db.prepare('UPDATE users SET notify_claim_tasks = ? WHERE id = ?').run(enabled, user.id);
511
+ await dbRun('UPDATE users SET notify_claim_tasks = ? WHERE id = ?', [enabled, user.id]);
480
512
  res.json({ success: true, notify_claim_tasks: enabled });
481
513
  });
482
- app.get('/api/me/notify-claim-tasks', (req, res) => {
514
+ app.get('/api/me/notify-claim-tasks', async (req, res) => {
483
515
  const user = auth(req, res);
484
516
  if (!user)
485
517
  return;
486
- const row = db.prepare('SELECT COALESCE(notify_claim_tasks, 1) as enabled FROM users WHERE id = ?').get(user.id);
518
+ const row = await dbOne('SELECT COALESCE(notify_claim_tasks, 1) as enabled FROM users WHERE id = ?', [user.id]);
487
519
  res.json({ notify_claim_tasks: row?.enabled ?? 1 });
488
520
  });
489
521
  // 公开 #claims 广场(无 auth — 透明性是验证声明信任的前提)
490
- app.get('/api/claims/public', (req, res) => {
522
+ app.get('/api/claims/public', async (req, res) => {
491
523
  const status = String(req.query.status || 'open');
492
524
  const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 30));
493
525
  let where;
@@ -508,7 +540,7 @@ export function registerClaimVerifyRoutes(app, deps) {
508
540
  where = `1=1`;
509
541
  orderBy = `cvt.created_at DESC`;
510
542
  }
511
- const rows = db.prepare(`
543
+ const rows = await dbAll(`
512
544
  SELECT cvt.id, cvt.order_id, cvt.product_id, cvt.claim_target,
513
545
  SUBSTR(cvt.claim_text, 1, 140) as claim_excerpt,
514
546
  cvt.evidence_uri IS NOT NULL as has_buyer_evidence,
@@ -522,7 +554,7 @@ export function registerClaimVerifyRoutes(app, deps) {
522
554
  WHERE ${where}
523
555
  ORDER BY ${orderBy}
524
556
  LIMIT ?
525
- `).all(limit);
557
+ `, [limit]);
526
558
  const items = rows.map(r => {
527
559
  let firstImage = null;
528
560
  try {
@@ -553,33 +585,30 @@ export function registerClaimVerifyRoutes(app, deps) {
553
585
  res.json({ items, votes_needed: CLAIM_VERIFIERS_NEEDED });
554
586
  });
555
587
  // 任务详情
556
- app.get('/api/claim-tasks/:id', (req, res) => {
588
+ app.get('/api/claim-tasks/:id', async (req, res) => {
557
589
  const user = auth(req, res);
558
590
  if (!user)
559
591
  return;
560
- const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?')
561
- .get(req.params.id);
592
+ const task = await dbOne('SELECT * FROM claim_verification_tasks WHERE id = ?', [req.params.id]);
562
593
  if (!task)
563
594
  return void res.status(404).json({ error: '任务不存在' });
564
- const hasVoted = db.prepare('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?')
565
- .get(req.params.id, user.id);
595
+ const hasVoted = await dbOne('SELECT id FROM claim_verification_votes WHERE task_id = ? AND verifier_id = ?', [req.params.id, user.id]);
566
596
  const isParty = task.buyer_id === user.id || task.seller_id === user.id;
567
597
  const elig = isEligibleClaimVerifier(db, user.id);
568
598
  const canRead = isParty || !!hasVoted || elig.ok;
569
599
  if (!canRead) {
570
600
  return void res.status(403).json({ error: '仅当事人或已投票 / 资格内 verifier 可见任务详情' });
571
601
  }
572
- const votes = db.prepare(`SELECT id, verifier_id, vote, evidence_uri, note, voted_at
573
- FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`).all(req.params.id);
602
+ const votes = await dbAll(`SELECT id, verifier_id, vote, evidence_uri, note, voted_at
603
+ FROM claim_verification_votes WHERE task_id = ? ORDER BY voted_at ASC`, [req.params.id]);
574
604
  res.json({ task, votes, votes_needed: CLAIM_VERIFIERS_NEEDED });
575
605
  });
576
606
  // 卖家提交证据 → 延期 24h;状态保持 open
577
- app.post('/api/claim-tasks/:id/seller-evidence', (req, res) => {
607
+ app.post('/api/claim-tasks/:id/seller-evidence', async (req, res) => {
578
608
  const user = auth(req, res);
579
609
  if (!user)
580
610
  return;
581
- const task = db.prepare('SELECT * FROM claim_verification_tasks WHERE id = ?')
582
- .get(req.params.id);
611
+ const task = await dbOne('SELECT * FROM claim_verification_tasks WHERE id = ?', [req.params.id]);
583
612
  if (!task)
584
613
  return void res.status(404).json({ error: '任务不存在' });
585
614
  if (task.seller_id !== user.id)
@@ -595,11 +624,15 @@ export function registerClaimVerifyRoutes(app, deps) {
595
624
  const oldDeadline = new Date(String(task.deadline_at)).getTime();
596
625
  const newCandidate = Date.now() + CLAIM_SELLER_EXTENSION_HOURS * 3600_000;
597
626
  const newDeadline = new Date(Math.max(oldDeadline, newCandidate)).toISOString();
598
- db.prepare(`UPDATE claim_verification_tasks
627
+ // Codex #237 P2:await 预检与写之间 task 可能被结算/并发提证;status='open' + seller_evidence_at IS NULL
628
+ // 守卫保证只在仍 open 且未提交过时写,changes===0 → 409。
629
+ const ev = await dbRun(`UPDATE claim_verification_tasks
599
630
  SET seller_evidence_uri = ?, seller_evidence_at = datetime('now'), deadline_at = ?
600
- WHERE id = ?`).run(evidence_uri, newDeadline, req.params.id);
631
+ WHERE id = ? AND status = 'open' AND seller_evidence_at IS NULL`, [evidence_uri, newDeadline, req.params.id]);
632
+ if (ev.changes === 0)
633
+ return void res.status(409).json({ error: '任务状态已变更或已提交过证据(请刷新)' });
601
634
  try {
602
- const productTitle = db.prepare('SELECT title FROM products WHERE id = ?').get(task.product_id)?.title || '—';
635
+ const productTitle = (await dbOne('SELECT title FROM products WHERE id = ?', [task.product_id]))?.title || '—';
603
636
  const claimLabel = CLAIM_TARGET_LABEL_ZH[String(task.claim_target)] || String(task.claim_target);
604
637
  notifyEligibleVerifiers(db, generateId, {
605
638
  taskId: String(task.id), productTitle, claimTargetLabel: claimLabel,
@@ -1,9 +1,13 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerClaimVotingRoutes(app, deps) {
3
+ // 只读站点走 RFC-016 异步 seam;db 保留:vote 是"投票→封顶→结算"裁决资金路径,
4
+ // dup 门 + INSERT vote + 计票 + seal-CAS 必须原子(db.transaction);settle(发还/没收质押)
5
+ // 在 tx 提交后只对真正 seal 的那一票触发,防并发双封顶/双结算。Phase 3 迁 pg 行锁。
2
6
  const { db, auth, isEligibleClaimVerifier, generateId, settleProductClaim, settleGenericClaim, PRODUCT_CLAIM_VERIFIERS_NEEDED, REVIEW_VERIFIERS_NEEDED } = deps;
3
7
  const wire = (cfg) => {
4
8
  const { vertical, taskTable, voteTable, taskAlias: a, partyIdCol, votePrefix, votesNeeded } = cfg;
5
9
  // GET /api/<vertical>-claims/available
6
- app.get(`/api/${vertical}-claims/available`, (req, res) => {
10
+ app.get(`/api/${vertical}-claims/available`, async (req, res) => {
7
11
  const user = auth(req, res);
8
12
  if (!user)
9
13
  return;
@@ -22,18 +26,18 @@ export function registerClaimVotingRoutes(app, deps) {
22
26
  AND (SELECT COUNT(*) FROM ${voteTable} WHERE claim_id = ${a}.id) < ${votesNeeded}
23
27
  ORDER BY ${a}.created_at ASC LIMIT 50
24
28
  `;
25
- const rows = db.prepare(sql).all(user.id, user.id, user.id);
29
+ const rows = await dbAll(sql, [user.id, user.id, user.id]);
26
30
  res.json({ items: rows, eligible: true });
27
31
  });
28
32
  // POST /api/<vertical>-claims/:id/vote
29
- app.post(`/api/${vertical}-claims/:id/vote`, (req, res) => {
33
+ app.post(`/api/${vertical}-claims/:id/vote`, async (req, res) => {
30
34
  const user = auth(req, res);
31
35
  if (!user)
32
36
  return;
33
37
  const elig = isEligibleClaimVerifier(user.id);
34
38
  if (!elig.ok)
35
39
  return void res.status(403).json({ error: elig.reason });
36
- const claim = db.prepare(`SELECT * FROM ${taskTable} WHERE id = ?`).get(req.params.id);
40
+ const claim = await dbOne(`SELECT * FROM ${taskTable} WHERE id = ?`, [req.params.id]);
37
41
  if (!claim)
38
42
  return void res.status(404).json({ error: '声明不存在' });
39
43
  if (claim.status !== 'open')
@@ -45,29 +49,50 @@ export function registerClaimVotingRoutes(app, deps) {
45
49
  if (!['upheld', 'dismissed', 'insufficient'].includes(vote)) {
46
50
  return void res.status(400).json({ error: `vote 须为 upheld / dismissed / insufficient` });
47
51
  }
48
- const dup = db.prepare(`SELECT id FROM ${voteTable} WHERE claim_id = ? AND verifier_id = ?`).get(req.params.id, user.id);
49
- if (dup)
50
- return void res.status(409).json({ error: '已投过票' });
51
- const votesNow = db.prepare(`SELECT COUNT(*) as n FROM ${voteTable} WHERE claim_id = ?`).get(req.params.id).n;
52
- if (votesNow >= votesNeeded)
53
- return void res.status(409).json({ error: '已收齐共识票数' });
54
52
  const evidence_uri = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
55
53
  const note = req.body?.note ? String(req.body.note).trim().slice(0, 500) : null;
56
54
  const voteId = generateId(votePrefix);
55
+ // 裁决原子段:权威重检(状态/dup/票数)→ INSERT vote → 重计票 → 达标则 CAS 封顶。
56
+ // 返回 { after, didSeal };didSeal 仅对真正把 open→sealed 翻过去的那一票为 true。
57
+ let txOut;
57
58
  try {
58
- db.prepare(`INSERT INTO ${voteTable} (id, claim_id, verifier_id, vote, evidence_uri, note) VALUES (?,?,?,?,?,?)`)
59
- .run(voteId, req.params.id, user.id, vote, evidence_uri, note);
59
+ txOut = db.transaction(() => {
60
+ const cur = db.prepare(`SELECT status FROM ${taskTable} WHERE id = ?`).get(req.params.id);
61
+ if (!cur || cur.status !== 'open')
62
+ throw new Error('VOTE_CLOSED');
63
+ if (db.prepare(`SELECT id FROM ${voteTable} WHERE claim_id = ? AND verifier_id = ?`).get(req.params.id, user.id))
64
+ throw new Error('VOTE_DUP');
65
+ const now = db.prepare(`SELECT COUNT(*) as n FROM ${voteTable} WHERE claim_id = ?`).get(req.params.id).n;
66
+ if (now >= votesNeeded)
67
+ throw new Error('VOTE_FULL');
68
+ db.prepare(`INSERT INTO ${voteTable} (id, claim_id, verifier_id, vote, evidence_uri, note) VALUES (?,?,?,?,?,?)`)
69
+ .run(voteId, req.params.id, user.id, vote, evidence_uri, note);
70
+ const after = db.prepare(`SELECT COUNT(*) as n FROM ${voteTable} WHERE claim_id = ?`).get(req.params.id).n;
71
+ let didSeal = false;
72
+ if (after >= votesNeeded) {
73
+ const seal = db.prepare(`UPDATE ${taskTable} SET status = 'sealed' WHERE id = ? AND status = 'open'`).run(req.params.id);
74
+ didSeal = seal.changes === 1;
75
+ }
76
+ return { after, didSeal };
77
+ })();
60
78
  }
61
- catch {
62
- return void res.status(409).json({ error: '投票失败(可能并发重复)' });
79
+ catch (e) {
80
+ const msg = e.message;
81
+ if (msg === 'VOTE_CLOSED')
82
+ return void res.status(400).json({ error: '该声明已结案,不接受投票' });
83
+ if (msg === 'VOTE_DUP')
84
+ return void res.status(409).json({ error: '已投过票' });
85
+ if (msg === 'VOTE_FULL')
86
+ return void res.status(409).json({ error: '已收齐共识票数' });
87
+ console.error('[claim-voting tx]', msg);
88
+ return void res.status(500).json({ error: '投票失败,请重试' });
63
89
  }
64
- const after = db.prepare(`SELECT COUNT(*) as n FROM ${voteTable} WHERE claim_id = ?`).get(req.params.id).n;
90
+ // settle(发还/没收质押)在事务提交后只对真正 seal 的那一票触发(它自身另起事务)
65
91
  let settlement = null;
66
- if (after >= votesNeeded) {
67
- db.prepare(`UPDATE ${taskTable} SET status = 'sealed' WHERE id = ? AND status = 'open'`).run(req.params.id);
92
+ if (txOut.didSeal) {
68
93
  settlement = cfg.useProductSettle ? settleProductClaim(req.params.id) : settleGenericClaim(taskTable, voteTable, req.params.id);
69
94
  }
70
- res.json({ success: true, votes_collected: after, sealed: after >= votesNeeded, settlement });
95
+ res.json({ success: true, votes_collected: txOut.after, sealed: txOut.didSeal, settlement });
71
96
  });
72
97
  };
73
98
  // 5 个垂类配置
@@ -0,0 +1,147 @@
1
+ import { z } from 'zod';
2
+ import { issueGithubIdentityClaimChallenge, getIssuedChallengeForVerification, } from '../../layer2-business/L2-9-contribution/identity-claim-challenge-engine.js';
3
+ import { verifyGithubGistProof } from '../../layer2-business/L2-9-contribution/identity-claim-proof-verifier.js';
4
+ import { claimGithubIdentity } from '../../layer2-business/L2-9-contribution/identity-claim-engine.js';
5
+ import { getMyGithubIdentitySurface } from '../../layer2-business/L2-9-contribution/identity-claim-read.js';
6
+ import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
7
+ // ── strict request bodies (unknown/sensitive keys → rejected; nothing trusts a caller field) ──
8
+ const ChallengeBody = z.strictObject({
9
+ source_event_key: z.string().min(1),
10
+ github_actor_id: z.string().min(1),
11
+ });
12
+ const CompleteBody = z.strictObject({
13
+ source_event_key: z.string().min(1),
14
+ github_actor_id: z.string().min(1),
15
+ challenge_id: z.string().min(1),
16
+ gist_id: z.string().min(1),
17
+ webauthn_token: z.string().min(1), // one-time WebAuthn gate token id (purpose 'identity_claim')
18
+ });
19
+ const PARAM_KEY = 'require_human_presence_for_identity_claim';
20
+ export function registerContributionIdentityRoutes(app, deps) {
21
+ const { auth, requireHumanPresence, errorRes, getGithubReadToken } = deps;
22
+ // ── 1) issue a publication challenge ─────────────────────────────────────────────────────────
23
+ app.post('/api/contribution-identity/github/claim-challenge', async (req, res) => {
24
+ const user = auth(req, res);
25
+ if (!user)
26
+ return;
27
+ const parsed = ChallengeBody.safeParse(req.body ?? {});
28
+ if (!parsed.success)
29
+ return void errorRes(res, 400, 'INVALID_REQUEST', '请求参数无效');
30
+ // accountId is ALWAYS the session user (never the body).
31
+ const r = await issueGithubIdentityClaimChallenge({
32
+ accountId: user.id,
33
+ githubActorId: parsed.data.github_actor_id,
34
+ sourceEventKey: parsed.data.source_event_key,
35
+ });
36
+ if (r.ok && r.status === 'issued') {
37
+ return void res.json({ status: 'issued', challenge_id: r.challenge_id, expires_at: r.expires_at, proof_marker: r.proof_marker });
38
+ }
39
+ if (r.ok && r.status === 'already_bound_self') {
40
+ return void res.json({ status: 'already_bound_self', github_actor_id: r.github_actor_id });
41
+ }
42
+ // refused — map to a status without leaking internals.
43
+ switch (r.reason) {
44
+ case 'invalid_request': return void errorRes(res, 400, 'INVALID_REQUEST', '请求参数无效');
45
+ case 'fact_not_found': return void errorRes(res, 404, 'FACT_NOT_CLAIMABLE', '没有可认领的、经凭证背书的 GitHub 贡献记录');
46
+ case 'actor_mismatch': return void errorRes(res, 403, 'ACTOR_MISMATCH', '该贡献记录的执行者与所声明的 GitHub 身份不符');
47
+ case 'already_bound_other': return void errorRes(res, 409, 'ALREADY_BOUND', '该 GitHub 身份已被其他账号认领');
48
+ case 'backend_unsupported': return void errorRes(res, 503, 'BACKEND_UNSUPPORTED', '当前后端暂不支持身份认领');
49
+ case 'db_busy': return void errorRes(res, 503, 'DB_BUSY', '系统繁忙,请稍后重试');
50
+ default: return void errorRes(res, 500, 'INTERNAL', '内部错误');
51
+ }
52
+ });
53
+ // ── 2) complete the claim (human gate → re-fetch gist proof → atomic consume+bind) ────────────
54
+ app.post('/api/contribution-identity/github/claim-complete', async (req, res) => {
55
+ const user = auth(req, res);
56
+ if (!user)
57
+ return;
58
+ const parsed = CompleteBody.safeParse(req.body ?? {});
59
+ if (!parsed.success)
60
+ return void errorRes(res, 400, 'INVALID_REQUEST', '请求参数无效');
61
+ const { source_event_key, github_actor_id, challenge_id, gist_id, webauthn_token } = parsed.data;
62
+ const userId = user.id;
63
+ // Server-config precondition FIRST — don't burn the one-time human gate token if the server can't
64
+ // perform the authenticated GitHub read (fail closed; issuing a challenge never needs a token).
65
+ const githubToken = getGithubReadToken();
66
+ if (!githubToken)
67
+ return void errorRes(res, 503, 'GITHUB_READ_NOT_CONFIGURED', '身份认领暂不可用');
68
+ // ① human presence — the gate token must be bound (purpose_data) to THIS exact claim tuple, so a
69
+ // token minted for one claim cannot complete another, and an agent cannot replay it.
70
+ const hp = requireHumanPresence(userId, 'identity_claim', webauthn_token, PARAM_KEY, (data) => {
71
+ const d = data;
72
+ return !!d && d.github_actor_id === github_actor_id && d.source_event_key === source_event_key && d.challenge_id === challenge_id;
73
+ });
74
+ if (!hp.ok)
75
+ return void errorRes(res, 412, hp.error_code || 'HUMAN_PRESENCE_REQUIRED', hp.reason || '此操作需真实人工 WebAuthn 验证');
76
+ // ② confirm the challenge is issued/owned/unexpired and fetch the stored nonce_hash (read-only;
77
+ // BEFORE the network call so a bad challenge never triggers a GitHub fetch). Not consumed here.
78
+ const look = getIssuedChallengeForVerification({ challengeId: challenge_id, accountId: userId, githubActorId: github_actor_id, sourceEventKey: source_event_key });
79
+ if (!look.ok) {
80
+ switch (look.reason) {
81
+ case 'challenge_not_found': return void errorRes(res, 404, 'CHALLENGE_NOT_FOUND', '认领挑战不存在或不属于当前账号');
82
+ case 'challenge_expired': return void errorRes(res, 410, 'CHALLENGE_EXPIRED', '认领挑战已过期,请重新发起');
83
+ case 'challenge_already_used': return void errorRes(res, 409, 'CHALLENGE_ALREADY_USED', '认领挑战已被使用');
84
+ case 'backend_unsupported': return void errorRes(res, 503, 'BACKEND_UNSUPPORTED', '当前后端暂不支持身份认领');
85
+ default: return void errorRes(res, 400, 'INVALID_REQUEST', '请求参数无效');
86
+ }
87
+ }
88
+ // ③ WebAZ re-fetches the gist itself (trusted token from config; NEVER the body) and verifies
89
+ // owner.id == actor + marker + sha256(nonce) == stored nonce_hash. A failure here does NOT consume
90
+ // the challenge (F2 is not called) — the user can fix the gist and retry.
91
+ const proof = await verifyGithubGistProof({
92
+ gistId: gist_id,
93
+ githubActorId: github_actor_id,
94
+ challengeId: challenge_id,
95
+ expectedNonceHash: look.nonceHash,
96
+ token: githubToken,
97
+ });
98
+ if (!proof.ok) {
99
+ // Surface only the typed outcome (verifier guarantees its reasons are token-free; we don't echo them).
100
+ const code = proof.outcome === 'rate_limited' ? 429
101
+ : proof.outcome === 'timeout' || proof.outcome === 'upstream_unavailable' ? 502
102
+ : proof.outcome === 'not_found' ? 404
103
+ : proof.outcome === 'invalid_request' ? 400
104
+ : 422;
105
+ return void errorRes(res, code, 'PROOF_REJECTED', '未能验证 GitHub 公开发布凭证', { proof_outcome: proof.outcome });
106
+ }
107
+ // ④ atomic consume(CAS) + bind — proofVerified:true; accountId is the session user.
108
+ const claim = await claimGithubIdentity({
109
+ accountId: userId,
110
+ githubActorId: github_actor_id,
111
+ sourceEventKey: source_event_key,
112
+ challengeId: challenge_id,
113
+ proofVerified: true,
114
+ });
115
+ if (claim.ok) {
116
+ return void res.json({ status: claim.status, github_actor_id: claim.github_actor_id, challenge_id: claim.challenge_id });
117
+ }
118
+ switch (claim.reason) {
119
+ case 'already_bound_other': return void errorRes(res, 409, 'ALREADY_BOUND', '该 GitHub 身份已被其他账号认领');
120
+ case 'challenge_already_used': return void errorRes(res, 409, 'CHALLENGE_ALREADY_USED', '认领挑战已被使用');
121
+ case 'challenge_expired': return void errorRes(res, 410, 'CHALLENGE_EXPIRED', '认领挑战已过期,请重新发起');
122
+ case 'challenge_not_found': return void errorRes(res, 404, 'CHALLENGE_NOT_FOUND', '认领挑战不存在或不属于当前账号');
123
+ case 'fact_not_found': return void errorRes(res, 404, 'FACT_NOT_CLAIMABLE', '没有可认领的、经凭证背书的 GitHub 贡献记录');
124
+ case 'actor_mismatch': return void errorRes(res, 403, 'ACTOR_MISMATCH', '该贡献记录的执行者与所声明的 GitHub 身份不符');
125
+ case 'backend_unsupported': return void errorRes(res, 503, 'BACKEND_UNSUPPORTED', '当前后端暂不支持身份认领');
126
+ case 'db_busy': return void errorRes(res, 503, 'DB_BUSY', '系统繁忙,请稍后重试');
127
+ default: return void errorRes(res, 500, 'INTERNAL', '内部错误'); // proof_not_verified / invariant_violation
128
+ }
129
+ });
130
+ // ── 3) READ-ONLY: the caller's OWN bindings + attributable facts (PR-F4) ──────────────────────
131
+ // No query/body input is read — accountId is ALWAYS the session user, so a caller cannot ask about
132
+ // another account or github_actor_id. Returns no other account's id, no token/nonce/nonce_hash.
133
+ // PR-5A: the response is wrapped in the uncommitted-value boundary (RFC-017 I-12 / §7) so this
134
+ // metering/display surface can never read as a payout promise — facts + attribution only.
135
+ app.get('/api/contribution-identity/github/me', async (req, res) => {
136
+ const user = auth(req, res);
137
+ if (!user)
138
+ return;
139
+ try {
140
+ const surface = await getMyGithubIdentitySurface(user.id);
141
+ res.json(withUncommittedValueBoundary(surface));
142
+ }
143
+ catch {
144
+ return void errorRes(res, 500, 'INTERNAL', '内部错误'); // never leak a stack / query
145
+ }
146
+ });
147
+ }
@@ -0,0 +1,19 @@
1
+ import { collectContributionScoreEvidence } from '../../layer2-business/L2-9-contribution/contribution-score-evidence.js';
2
+ import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
3
+ export function registerContributionScoreRoutes(app, deps) {
4
+ const { auth, errorRes } = deps;
5
+ // READ-ONLY self-view of contribution-score EVIDENCE (not a score). No query/body input — accountId is
6
+ // always the session user. Output is component evidence wrapped in the uncommitted-value boundary.
7
+ app.get('/api/contribution-score/evidence/me', async (req, res) => {
8
+ const user = auth(req, res);
9
+ if (!user)
10
+ return;
11
+ try {
12
+ const components = await collectContributionScoreEvidence(user.id);
13
+ res.json(withUncommittedValueBoundary({ evidence_version: 'v1', components }));
14
+ }
15
+ catch {
16
+ return void errorRes(res, 500, 'INTERNAL', '内部错误'); // never leak a stack / query
17
+ }
18
+ });
19
+ }