@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,6 +1,8 @@
1
1
  // RFC-014 PR6 — 拍卖 stake 锁定/释放走整数 base-units + 绝对值落库。
2
2
  import { toUnits } from '../../money.js';
3
3
  import { applyWalletDelta } from '../../ledger.js';
4
+ // RFC-016 Phase 1 — 纯校验读/公开读/读回 → async seam;db.transaction 内 stake 写序列 + cron 保持同步。
5
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
4
6
  // ─── 拍卖常量(域内)──────────────────────────────────────────
5
7
  const AUC_MAX_WINDOW_MIN = 14 * 24 * 60; // 14 天上限
6
8
  const AUC_MIN_WINDOW_MIN = 5;
@@ -54,9 +56,9 @@ export function fireDueAuctionReminders(db, generateId) {
54
56
  return { fired };
55
57
  }
56
58
  export function registerAuctionRoutes(app, deps) {
57
- const { db, auth, generateId, RFQ_MAX_QTY, RFQ_MAX_PRICE, LISTING_CATEGORIES, isListingCategoryKey, requireProtocolAdmin } = deps;
59
+ const { db, auth, generateId, RFQ_MAX_QTY, RFQ_MAX_PRICE, LISTING_CATEGORIES, isListingCategoryKey, requireProtocolAdmin, logAdminAction } = deps;
58
60
  // 卖家发起拍卖
59
- app.post('/api/auctions', (req, res) => {
61
+ app.post('/api/auctions', async (req, res) => {
60
62
  const user = auth(req, res);
61
63
  if (!user)
62
64
  return;
@@ -90,12 +92,12 @@ export function registerAuctionRoutes(app, deps) {
90
92
  const windowMin = Math.max(AUC_MIN_WINDOW_MIN, Math.min(AUC_MAX_WINDOW_MIN, Math.floor(Number(body.window_min || AUC_DEFAULT_WINDOW_MIN))));
91
93
  const sniperExtend = Math.max(0, Math.min(60, Math.floor(Number(body.sniper_extend_min ?? AUC_DEFAULT_SNIPER_MIN))));
92
94
  // 频率限制
93
- const today = db.prepare("SELECT COUNT(1) as n FROM auctions WHERE seller_id = ? AND created_at > datetime('now','-1 day')").get(user.id).n;
95
+ const today = (await dbOne("SELECT COUNT(1) as n FROM auctions WHERE seller_id = ? AND created_at > datetime('now','-1 day')", [user.id])).n;
94
96
  if (today >= AUC_DAILY_CAP_PER_SELLER)
95
97
  return void res.json({ error: `今日已达上限 ${AUC_DAILY_CAP_PER_SELLER} 场拍卖` });
96
98
  // 卖家担保金
97
99
  const sellerStake = aucSellerStake(startingPrice);
98
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
100
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
99
101
  if (!wallet || Number(wallet.balance) < sellerStake) {
100
102
  return void res.json({ error: `余额不足,卖家担保金 ${sellerStake} WAZ(5% × 起拍价)` });
101
103
  }
@@ -103,7 +105,7 @@ export function registerAuctionRoutes(app, deps) {
103
105
  let productId = null;
104
106
  if (body.product_id) {
105
107
  productId = String(body.product_id);
106
- const p = db.prepare("SELECT seller_id, stock, status FROM products WHERE id = ?").get(productId);
108
+ const p = await dbOne("SELECT seller_id, stock, status FROM products WHERE id = ?", [productId]);
107
109
  if (!p)
108
110
  return void res.json({ error: '关联商品不存在' });
109
111
  if (p.seller_id !== user.id)
@@ -114,19 +116,43 @@ export function registerAuctionRoutes(app, deps) {
114
116
  return void res.json({ error: `库存不足(${p.stock} < ${qty})` });
115
117
  }
116
118
  const id = generateId('auc');
117
- db.transaction(() => {
118
- db.prepare(`
119
- INSERT INTO auctions (id, seller_id, listing_id, product_id, title, spec_json, qty, category,
120
- starting_price, current_price, min_increment, reserve_price, deadline_at, sniper_extend_min,
121
- seller_stake_locked, notes)
122
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
123
- `).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
124
- applyWalletDelta(db, user.id, { balance: -toUnits(sellerStake), staked: toUnits(sellerStake) });
125
- if (productId)
126
- db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ?").run(productId);
127
- })();
128
- // QA 轮 12 P1:返回完整 echo 字段 + ISO deadline_at 与 detail 一致
129
- const created = db.prepare('SELECT deadline_at, status FROM auctions WHERE id = ?').get(id);
119
+ try {
120
+ db.transaction(() => {
121
+ // 余额守恒 guard(Codex PR#228 P1):tx 内重读余额并在任何写之前判定。
122
+ // 上面 `await dbOne` 预检与同步 stake tx 之间有 yield,并发请求可都通过陈旧余额预检后
123
+ // 双双锁押 → 超额。同步 tx 体内无 yield,这次重读反映已提交的扣减,失败即抛回滚。
124
+ const wTx = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
125
+ if (!wTx || Number(wTx.balance) < sellerStake)
126
+ throw new Error('AUC_INSUFFICIENT');
127
+ db.prepare(`
128
+ INSERT INTO auctions (id, seller_id, listing_id, product_id, title, spec_json, qty, category,
129
+ starting_price, current_price, min_increment, reserve_price, deadline_at, sniper_extend_min,
130
+ seller_stake_locked, notes)
131
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
132
+ `).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
133
+ applyWalletDelta(db, user.id, { balance: -toUnits(sellerStake), staked: toUnits(sellerStake) });
134
+ // 商品状态机 CAS(Codex follow-up #239):active→auction_pending 必须带 status 守卫。
135
+ // 上面 `await dbOne` 校验 status='active' 与同步 tx 间有 yield,并发 create 可都通过陈旧
136
+ // 'active' 预检 → 同一商品被双双挂进两个拍卖。CAS changes=0 即已被他人移走,抛回滚。
137
+ if (productId) {
138
+ const flipped = db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ? AND status = 'active'").run(productId);
139
+ if (flipped.changes === 0)
140
+ throw new Error('AUC_PRODUCT_CONFLICT');
141
+ }
142
+ })();
143
+ }
144
+ catch (e) {
145
+ const msg = e.message;
146
+ if (msg === 'AUC_INSUFFICIENT') {
147
+ return void res.json({ error: `余额不足,卖家担保金 ${sellerStake} WAZ(5% × 起拍价)` });
148
+ }
149
+ if (msg === 'AUC_PRODUCT_CONFLICT') {
150
+ return void res.json({ error: '关联商品状态已变更(可能已被上架到其它拍卖或下架),请刷新后重试' });
151
+ }
152
+ throw e;
153
+ }
154
+ // QA 轮 12 P1:返回完整 echo 字段 + ISO deadline_at 与 detail 一致(tx 后纯读回)
155
+ const created = (await dbOne('SELECT deadline_at, status FROM auctions WHERE id = ?', [id]));
130
156
  res.json({
131
157
  id,
132
158
  seller_stake: sellerStake,
@@ -145,7 +171,7 @@ export function registerAuctionRoutes(app, deps) {
145
171
  });
146
172
  });
147
173
  // 看板:浏览公开拍卖(匿名可访问)
148
- app.get('/api/auctions', (req, res) => {
174
+ app.get('/api/auctions', async (req, res) => {
149
175
  const where = ["a.status = 'open'", "a.deadline_at > datetime('now')"];
150
176
  const args = [];
151
177
  if (req.query.category) {
@@ -159,7 +185,7 @@ export function registerAuctionRoutes(app, deps) {
159
185
  args.push(like, like);
160
186
  }
161
187
  const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
162
- const rows = db.prepare(`
188
+ const rows = await dbAll(`
163
189
  SELECT a.id, a.seller_id, a.title, a.qty, a.category, a.starting_price, a.current_price,
164
190
  a.min_increment, a.reserve_price, a.deadline_at, a.bid_count, a.sniper_extend_min, a.created_at,
165
191
  u.handle as seller_handle, u.region as seller_region
@@ -168,42 +194,42 @@ export function registerAuctionRoutes(app, deps) {
168
194
  WHERE ${where.join(' AND ')}
169
195
  ORDER BY a.deadline_at ASC
170
196
  LIMIT ?
171
- `).all(...args, limit);
197
+ `, [...args, limit]);
172
198
  res.json({ items: rows, categories: LISTING_CATEGORIES });
173
199
  });
174
200
  // 我的:买家=我出过价的,卖家=我发起的
175
- app.get('/api/auctions/mine', (req, res) => {
201
+ app.get('/api/auctions/mine', async (req, res) => {
176
202
  const user = auth(req, res);
177
203
  if (!user)
178
204
  return;
179
- const seller = db.prepare(`SELECT * FROM auctions WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
180
- const buyer = db.prepare(`
205
+ const seller = await dbAll(`SELECT * FROM auctions WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`, [user.id]);
206
+ const buyer = await dbAll(`
181
207
  SELECT DISTINCT a.*, (SELECT b.price FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_bid,
182
208
  (SELECT b.status FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_status
183
209
  FROM auctions a
184
210
  JOIN auction_bids b ON b.auction_id = a.id
185
211
  WHERE b.buyer_id = ? ORDER BY a.created_at DESC LIMIT 50
186
- `).all(user.id, user.id, user.id);
212
+ `, [user.id, user.id, user.id]);
187
213
  res.json({ as_seller: seller, as_buyer: buyer });
188
214
  });
189
215
  // 详情:含 bid 历史(buyer 身份脱敏;卖家+出价人本人 可见全名)
190
- app.get('/api/auctions/:id', (req, res) => {
216
+ app.get('/api/auctions/:id', async (req, res) => {
191
217
  const user = auth(req, res);
192
218
  if (!user)
193
219
  return;
194
- const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
220
+ const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
195
221
  if (!auc)
196
222
  return void res.status(404).json({ error: '拍卖不存在' });
197
223
  const isSellerSelf = auc.seller_id === user.id;
198
224
  const isSettled = auc.status !== 'open';
199
- const bids = db.prepare(`
225
+ const bids = await dbAll(`
200
226
  SELECT b.id, b.buyer_id, b.price, b.stake_locked, b.status, b.submitted_at, b.resolved_at,
201
227
  u.handle as buyer_handle
202
228
  FROM auction_bids b
203
229
  LEFT JOIN users u ON u.id = b.buyer_id
204
230
  WHERE b.auction_id = ?
205
231
  ORDER BY b.price DESC, b.submitted_at ASC
206
- `).all(req.params.id);
232
+ `, [req.params.id]);
207
233
  // 脱敏:非 (卖家/拍卖结束/出价人本人) 时,buyer_id 用后 6 位 + handle 隐藏
208
234
  const safeBids = bids.map(b => {
209
235
  const isMine = b.buyer_id === user.id;
@@ -218,11 +244,11 @@ export function registerAuctionRoutes(app, deps) {
218
244
  res.json({ auction: auc, bids: safeBids, is_seller: isSellerSelf });
219
245
  });
220
246
  // 拍卖「⏰ 提醒我」(#959)
221
- app.post('/api/auctions/:id/remind', (req, res) => {
247
+ app.post('/api/auctions/:id/remind', async (req, res) => {
222
248
  const user = auth(req, res);
223
249
  if (!user)
224
250
  return;
225
- const aucRow = db.prepare("SELECT id, deadline_at, status, seller_id FROM auctions WHERE id = ?").get(req.params.id);
251
+ const aucRow = await dbOne("SELECT id, deadline_at, status, seller_id FROM auctions WHERE id = ?", [req.params.id]);
226
252
  if (!aucRow)
227
253
  return void res.status(404).json({ error: '拍卖不存在' });
228
254
  if (aucRow.seller_id === user.id)
@@ -246,28 +272,28 @@ export function registerAuctionRoutes(app, deps) {
246
272
  tx();
247
273
  res.json({ ok: true, subscribed: true, leads_minutes: AUCTION_REMINDER_LEADS });
248
274
  });
249
- app.delete('/api/auctions/:id/remind', (req, res) => {
275
+ app.delete('/api/auctions/:id/remind', async (req, res) => {
250
276
  const user = auth(req, res);
251
277
  if (!user)
252
278
  return;
253
- const r = db.prepare("DELETE FROM auction_reminders WHERE auction_id = ? AND user_id = ?").run(req.params.id, user.id);
279
+ const r = await dbRun("DELETE FROM auction_reminders WHERE auction_id = ? AND user_id = ?", [req.params.id, user.id]);
254
280
  res.json({ ok: true, deleted: r.changes });
255
281
  });
256
- app.get('/api/auctions/:id/remind', (req, res) => {
282
+ app.get('/api/auctions/:id/remind', async (req, res) => {
257
283
  const user = auth(req, res);
258
284
  if (!user)
259
285
  return;
260
- const rows = db.prepare("SELECT lead_minutes, fire_at, sent_at FROM auction_reminders WHERE auction_id = ? AND user_id = ? ORDER BY lead_minutes DESC").all(req.params.id, user.id);
286
+ const rows = await dbAll("SELECT lead_minutes, fire_at, sent_at FROM auction_reminders WHERE auction_id = ? AND user_id = ? ORDER BY lead_minutes DESC", [req.params.id, user.id]);
261
287
  res.json({ subscribed: rows.length > 0, reminders: rows });
262
288
  });
263
289
  // 买家:出价
264
- app.post('/api/auctions/:id/bids', (req, res) => {
290
+ app.post('/api/auctions/:id/bids', async (req, res) => {
265
291
  const user = auth(req, res);
266
292
  if (!user)
267
293
  return;
268
294
  if (user.role !== 'buyer')
269
295
  return void res.json({ error: '仅买家可出价' });
270
- const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
296
+ const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
271
297
  if (!auc)
272
298
  return void res.status(404).json({ error: '拍卖不存在' });
273
299
  if (auc.status !== 'open')
@@ -298,10 +324,10 @@ export function registerAuctionRoutes(app, deps) {
298
324
  }
299
325
  const qty = Math.max(1, Math.floor(Number(auc.qty || 1)));
300
326
  const stake = aucBuyerStake(price, qty);
301
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
327
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
302
328
  // QA 轮 12 P1:自我加价 affordability check 应含已锁旧 stake(会在 tx 内释放)
303
329
  // 否则用户必须 ≥ 2× stake 余额才能加价,UX 卡。
304
- const myExisting = db.prepare("SELECT stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
330
+ const myExisting = await dbOne("SELECT stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'", [req.params.id, user.id]);
305
331
  const myExistingStake = Number(myExisting?.stake_locked || 0);
306
332
  const availableForBid = Number(wallet?.balance || 0) + myExistingStake;
307
333
  if (!wallet || availableForBid < stake) {
@@ -313,104 +339,117 @@ export function registerAuctionRoutes(app, deps) {
313
339
  let newDeadlineExt = null;
314
340
  let sellerTopup = 0;
315
341
  let closedErr = '';
316
- db.transaction(() => {
317
- // P1 #4:TX 内重读 auction 状态 + deadline 防 TOCTOU
318
- const fresh = db.prepare('SELECT status, deadline_at, current_price, bid_count, seller_stake_locked, max_extends, extends_used, sniper_extend_min FROM auctions WHERE id = ?').get(req.params.id);
319
- if (!fresh) {
320
- closedErr = 'not_found';
321
- return;
322
- }
323
- if (fresh.status !== 'open') {
324
- closedErr = `closed_${fresh.status}`;
325
- return;
326
- }
327
- if (fresh.deadline_at <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
328
- closedErr = 'expired';
329
- return;
330
- }
331
- // 价格重判(中间可能有别人插队)
332
- const curFresh = Number(fresh.current_price);
333
- const isFirstFresh = Number(fresh.bid_count) === 0;
334
- if (isFirstFresh) {
335
- if (price < startingPrice) {
336
- closedErr = `below_starting_${startingPrice}`;
342
+ try {
343
+ db.transaction(() => {
344
+ // P1 #4:TX 内重读 auction 状态 + deadline TOCTOU
345
+ const fresh = db.prepare('SELECT status, deadline_at, current_price, bid_count, seller_stake_locked, max_extends, extends_used, sniper_extend_min FROM auctions WHERE id = ?').get(req.params.id);
346
+ if (!fresh) {
347
+ closedErr = 'not_found';
337
348
  return;
338
349
  }
339
- }
340
- else {
341
- const minNeed = Math.round((curFresh + Number(auc.min_increment)) * 100) / 100;
342
- if (price < minNeed) {
343
- closedErr = `below_min_${minNeed}`;
350
+ if (fresh.status !== 'open') {
351
+ closedErr = `closed_${fresh.status}`;
344
352
  return;
345
353
  }
346
- }
347
- // 释放本人之前的 active bid
348
- const myPrev = db.prepare("SELECT id, stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
349
- if (myPrev) {
350
- db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
351
- if (myPrev.stake_locked > 0)
352
- applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
353
- }
354
- // 释放别人的最高 active bid
355
- const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
356
- for (const o of others) {
357
- db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
358
- if (o.stake_locked > 0)
359
- applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
360
- }
361
- // 插入新 bid
362
- db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
363
- .run(id, req.params.id, user.id, price, stake);
364
- applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
365
- // P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
366
- const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
367
- const curSellerStake = Number(fresh.seller_stake_locked) || 0;
368
- if (targetSellerStake > curSellerStake) {
369
- const delta = Math.round((targetSellerStake - curSellerStake) * 100) / 100;
370
- const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
371
- const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
372
- if (canTopup > 0) {
373
- applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
374
- db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
375
- sellerTopup = canTopup;
354
+ if (fresh.deadline_at <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
355
+ closedErr = 'expired';
356
+ return;
376
357
  }
377
- }
378
- // P1 #5:反狙击延长(max_extends 上限保护)
379
- const sniperMin = Number(fresh.sniper_extend_min || 0);
380
- const deadlineMs = Date.parse(String(fresh.deadline_at).replace(' ', 'T') + 'Z');
381
- const nowMs = Date.now();
382
- const inSnipeWindow = sniperMin > 0 && deadlineMs - nowMs < sniperMin * 60_000;
383
- const canExtend = Number(fresh.extends_used) < Number(fresh.max_extends || 10);
384
- if (inSnipeWindow && canExtend) {
385
- newDeadlineExt = sniperMin;
386
- db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1,
358
+ // 价格重判(中间可能有别人插队)
359
+ const curFresh = Number(fresh.current_price);
360
+ const isFirstFresh = Number(fresh.bid_count) === 0;
361
+ if (isFirstFresh) {
362
+ if (price < startingPrice) {
363
+ closedErr = `below_starting_${startingPrice}`;
364
+ return;
365
+ }
366
+ }
367
+ else {
368
+ const minNeed = Math.round((curFresh + Number(auc.min_increment)) * 100) / 100;
369
+ if (price < minNeed) {
370
+ closedErr = `below_min_${minNeed}`;
371
+ return;
372
+ }
373
+ }
374
+ // 释放本人之前的 active bid
375
+ const myPrev = db.prepare("SELECT id, stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
376
+ if (myPrev) {
377
+ db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
378
+ if (myPrev.stake_locked > 0)
379
+ applyWalletDelta(db, user.id, { balance: toUnits(myPrev.stake_locked), staked: -toUnits(myPrev.stake_locked) });
380
+ }
381
+ // 释放别人的最高 active bid
382
+ const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
383
+ for (const o of others) {
384
+ db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
385
+ if (o.stake_locked > 0)
386
+ applyWalletDelta(db, o.buyer_id, { balance: toUnits(o.stake_locked), staked: -toUnits(o.stake_locked) });
387
+ }
388
+ // 余额守恒 guard(Codex PR#228 P1):tx 内、释放本人旧 stake 之后、锁新 stake 之前重读余额。
389
+ // 上面 `await dbOne` 预检与同步 tx 间的 yield 让并发请求都通过陈旧余额 → 双双锁押超额。
390
+ // 此时 balance 已含本人旧 stake 的释放(若有),等价于预检的 availableForBid;不足即抛回滚。
391
+ const wTx = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
392
+ if (!wTx || Number(wTx.balance) < stake)
393
+ throw new Error('AUC_INSUFFICIENT');
394
+ // 插入新 bid
395
+ db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
396
+ .run(id, req.params.id, user.id, price, stake);
397
+ applyWalletDelta(db, user.id, { balance: -toUnits(stake), staked: toUnits(stake) });
398
+ // P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
399
+ const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
400
+ const curSellerStake = Number(fresh.seller_stake_locked) || 0;
401
+ if (targetSellerStake > curSellerStake) {
402
+ const delta = Math.round((targetSellerStake - curSellerStake) * 100) / 100;
403
+ const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
404
+ const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
405
+ if (canTopup > 0) {
406
+ applyWalletDelta(db, auc.seller_id, { balance: -toUnits(canTopup), staked: toUnits(canTopup) });
407
+ db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
408
+ sellerTopup = canTopup;
409
+ }
410
+ }
411
+ // P1 #5:反狙击延长(max_extends 上限保护)
412
+ const sniperMin = Number(fresh.sniper_extend_min || 0);
413
+ const deadlineMs = Date.parse(String(fresh.deadline_at).replace(' ', 'T') + 'Z');
414
+ const nowMs = Date.now();
415
+ const inSnipeWindow = sniperMin > 0 && deadlineMs - nowMs < sniperMin * 60_000;
416
+ const canExtend = Number(fresh.extends_used) < Number(fresh.max_extends || 10);
417
+ if (inSnipeWindow && canExtend) {
418
+ newDeadlineExt = sniperMin;
419
+ db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1,
387
420
  deadline_at = datetime(deadline_at, '+' || ? || ' minutes'),
388
421
  extends_used = extends_used + 1,
389
422
  updated_at = datetime('now') WHERE id = ?`).run(price, sniperMin, req.params.id);
423
+ }
424
+ else {
425
+ db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1, updated_at = datetime('now') WHERE id = ?`).run(price, req.params.id);
426
+ }
427
+ })();
428
+ }
429
+ catch (e) {
430
+ if (e.message === 'AUC_INSUFFICIENT') {
431
+ return void res.json({ error: `余额不足,出价押金 ${stake} WAZ(被超越后立即释放)` });
390
432
  }
391
- else {
392
- db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1, updated_at = datetime('now') WHERE id = ?`).run(price, req.params.id);
393
- }
394
- })();
433
+ throw e;
434
+ }
395
435
  if (closedErr) {
396
436
  const ce = closedErr;
397
437
  return void res.json({ error: ce === 'expired' ? '拍卖已到期' : ce === 'not_found' ? '拍卖不存在' : ce.startsWith('below_') ? '出价不足,请刷新页面查看当前最高价' : `拍卖已结束(${ce})` });
398
438
  }
399
- // 通知卖家 + 被超越的买家
439
+ // 通知卖家 + 被超越的买家(tx 后 fire-and-forget 单写 → seam)
400
440
  try {
401
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
402
- VALUES (?,?,'auction_new_bid',?,?,datetime('now'))`)
403
- .run(generateId('ntf'), auc.seller_id, `🔨 新出价 ${price} WAZ`, `拍卖:${String(auc.title).slice(0, 30)}`);
441
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
442
+ VALUES (?,?,'auction_new_bid',?,?,datetime('now'))`, [generateId('ntf'), auc.seller_id, `🔨 新出价 ${price} WAZ`, `拍卖:${String(auc.title).slice(0, 30)}`]);
404
443
  }
405
444
  catch { }
406
445
  res.json({ id, stake_locked: stake, current_price: price, sniper_extended_min: newDeadlineExt, seller_topup: sellerTopup || undefined });
407
446
  });
408
447
  // 卖家:取消(仅未出价时)
409
- app.delete('/api/auctions/:id', (req, res) => {
448
+ app.delete('/api/auctions/:id', async (req, res) => {
410
449
  const user = auth(req, res);
411
450
  if (!user)
412
451
  return;
413
- const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
452
+ const auc = await dbOne('SELECT * FROM auctions WHERE id = ?', [req.params.id]);
414
453
  if (!auc)
415
454
  return void res.status(404).json({ error: '拍卖不存在' });
416
455
  if (auc.seller_id !== user.id)
@@ -434,6 +473,8 @@ export function registerAuctionRoutes(app, deps) {
434
473
  const admin = requireProtocolAdmin(req, res);
435
474
  if (!admin)
436
475
  return;
437
- res.json(fireDueAuctionReminders(db, generateId));
476
+ const result = fireDueAuctionReminders(db, generateId);
477
+ logAdminAction(admin.id, 'auction_reminders_run', 'protocol', null, { result });
478
+ res.json(result);
438
479
  });
439
480
  }
@@ -1,13 +1,15 @@
1
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerAuthLoginRoutes(app, deps) {
2
- const { db, INTERNAL_AUDITOR_ID, isLocked, verifyPassword, recordFailure, resetFailures, recordSession } = deps;
3
- app.post('/api/login', (req, res) => {
3
+ // db 已全量走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
4
+ const { INTERNAL_AUDITOR_ID, isLocked, verifyPassword, recordFailure, resetFailures, recordSession } = deps;
5
+ app.post('/api/login', async (req, res) => {
4
6
  const { name, password } = req.body;
5
7
  if (!name?.trim() || !password)
6
8
  return void res.json({ error: '请填写用户名 / 昵称和密码' });
7
9
  const ref = name.trim().replace(/^@/, '').toLowerCase();
8
- let matches = db.prepare("SELECT * FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)").all(ref, INTERNAL_AUDITOR_ID);
10
+ let matches = await dbAll("SELECT * FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)", [ref, INTERNAL_AUDITOR_ID]);
9
11
  if (matches.length === 0) {
10
- matches = db.prepare("SELECT * FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)").all(name.trim(), INTERNAL_AUDITOR_ID);
12
+ matches = await dbAll("SELECT * FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)", [name.trim(), INTERNAL_AUDITOR_ID]);
11
13
  }
12
14
  if (matches.length === 0)
13
15
  return void res.json({ error: '账号或密码错误' });
@@ -1,10 +1,12 @@
1
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerAuthReadRoutes(app, deps) {
2
- const { db, auth, safeRoles, getRegionMaxLevels, userMlmGate, getUserLevel } = deps;
3
- app.get('/api/me', (req, res) => {
3
+ // db 已全量走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
4
+ const { auth, safeRoles, getRegionMaxLevels, userMlmGate, getUserLevel } = deps;
5
+ app.get('/api/me', async (req, res) => {
4
6
  const user = auth(req, res);
5
7
  if (!user)
6
8
  return;
7
- const wallet = db.prepare('SELECT * FROM wallets WHERE user_id = ?').get(user.id);
9
+ const wallet = await dbOne('SELECT * FROM wallets WHERE user_id = ?', [user.id]);
8
10
  let roles = [];
9
11
  try {
10
12
  roles = JSON.parse(user.roles || JSON.stringify([user.role]));
@@ -14,17 +16,25 @@ export function registerAuthReadRoutes(app, deps) {
14
16
  }
15
17
  const region = user.region || 'global';
16
18
  const maxLevels = getRegionMaxLevels(region);
17
- const pvEnabled = db.prepare("SELECT pv_enabled FROM region_config WHERE region = ?").get(region)?.pv_enabled ?? 0;
18
- res.json({ ...user, api_key: undefined, roles, wallet: wallet || null, region_max_levels: maxLevels, region_pv_enabled: Number(pvEnabled) === 1 ? 1 : 0 });
19
+ const pvEnabled = (await dbOne("SELECT pv_enabled FROM region_config WHERE region = ?", [region]))?.pv_enabled ?? 0;
20
+ // 恢复能力标志(供首页"无恢复方式"横幅 + 凭证清单徽章用)。password_hash 不外泄,只回布尔。
21
+ const passkeyCount = (await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [user.id]))?.n ?? 0;
22
+ res.json({
23
+ ...user, api_key: undefined, password_hash: undefined,
24
+ roles, wallet: wallet || null, region_max_levels: maxLevels, region_pv_enabled: Number(pvEnabled) === 1 ? 1 : 0,
25
+ email_verified: !!user.email_verified,
26
+ has_password: !!user.password_hash,
27
+ has_passkey: Number(passkeyCount) > 0,
28
+ });
19
29
  });
20
- app.get('/api/profile', (req, res) => {
30
+ app.get('/api/profile', async (req, res) => {
21
31
  const user = auth(req, res);
22
32
  if (!user)
23
33
  return;
24
- const wallet = db.prepare('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?').get(user.id);
34
+ const wallet = await dbOne('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?', [user.id]);
25
35
  const roles = safeRoles(user);
26
- const pv = db.prepare("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?").get(user.id);
27
- const pendingScore = db.prepare("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL").get(user.id).s;
36
+ const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [user.id]);
37
+ const pendingScore = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [user.id])).s;
28
38
  res.json({
29
39
  id: user.id, name: user.name, role: user.role, roles, api_key: user.api_key, wallet: wallet || null,
30
40
  permanent_code: user.permanent_code ?? null,
@@ -41,9 +51,10 @@ export function registerAuthReadRoutes(app, deps) {
41
51
  phone: user.phone ?? null,
42
52
  phone_verified: !!user.phone_verified,
43
53
  has_password: !!user.password_hash,
54
+ has_passkey: ((await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [user.id]))?.n ?? 0) > 0,
44
55
  region: user.region ?? 'global',
45
56
  region_max_levels: getRegionMaxLevels(user.region || 'global'),
46
- region_pv_enabled: (db.prepare("SELECT pv_enabled FROM region_config WHERE region = ?").get(user.region || 'global')?.pv_enabled ?? 0) === 1 ? 1 : 0,
57
+ region_pv_enabled: (((await dbOne("SELECT pv_enabled FROM region_config WHERE region = ?", [user.region || 'global']))?.pv_enabled ?? 0) === 1 ? 1 : 0),
47
58
  ...(() => { const g = userMlmGate(user.region || 'global'); return { mlm_ui_visible: g.mlmUiVisible, mlm_payout_levels: g.payoutLevels }; })(),
48
59
  bio: user.bio ?? null,
49
60
  search_anchor: user.search_anchor ?? null,