@seasonkoh/webaz 0.1.23 → 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 +198 -83
  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,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) => {
@@ -1,21 +1,23 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerBuyerFeedsRoutes(app, deps) {
2
- const { db, auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
3
- app.get('/api/recommendations/me', (req, res) => {
3
+ // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
+ const { auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
5
+ app.get('/api/recommendations/me', async (req, res) => {
4
6
  const user = auth(req, res);
5
7
  if (!user)
6
8
  return;
7
9
  if (isTrustedRole(user))
8
10
  return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
9
11
  const limit = Math.min(30, Math.max(5, Number(req.query.limit) || 20));
10
- const wishlistRows = db.prepare(`
12
+ const wishlistRows = await dbAll(`
11
13
  SELECT w.product_id, p.category, p.seller_id FROM user_wishlist w
12
14
  JOIN products p ON p.id = w.product_id
13
15
  WHERE w.user_id = ? AND p.status = 'active' LIMIT 50
14
- `).all(user.id);
15
- const purchasedRows = db.prepare(`
16
+ `, [user.id]);
17
+ const purchasedRows = await dbAll(`
16
18
  SELECT DISTINCT product_id, seller_id FROM orders WHERE buyer_id = ? AND status = 'completed' LIMIT 200
17
- `).all(user.id);
18
- const followedRows = db.prepare(`SELECT followee_id FROM follows WHERE follower_id = ?`).all(user.id);
19
+ `, [user.id]);
20
+ const followedRows = await dbAll(`SELECT followee_id FROM follows WHERE follower_id = ?`, [user.id]);
19
21
  const wishCats = new Set(wishlistRows.map(r => r.category).filter(Boolean));
20
22
  const knownProductIds = new Set([...wishlistRows.map(r => r.product_id), ...purchasedRows.map(r => r.product_id)]);
21
23
  const knownSellerIds = new Set([...wishlistRows.map(r => r.seller_id), ...purchasedRows.map(r => r.seller_id)]);
@@ -31,40 +33,40 @@ export function registerBuyerFeedsRoutes(app, deps) {
31
33
  let followedProducts = [];
32
34
  if (followedSellerIds.length > 0) {
33
35
  const ph = followedSellerIds.map(() => '?').join(',');
34
- followedProducts = db.prepare(`
36
+ followedProducts = await dbAll(`
35
37
  SELECT ${baseCols}
36
38
  FROM products p JOIN users u ON u.id = p.seller_id
37
39
  WHERE p.seller_id IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
38
40
  ORDER BY p.created_at DESC LIMIT 10
39
- `).all(...followedSellerIds, ...exclArgs);
41
+ `, [...followedSellerIds, ...exclArgs]);
40
42
  }
41
43
  let categoryProducts = [];
42
44
  if (wishCats.size > 0) {
43
45
  const ph = [...wishCats].map(() => '?').join(',');
44
- categoryProducts = db.prepare(`
46
+ categoryProducts = await dbAll(`
45
47
  SELECT ${baseCols}
46
48
  FROM products p JOIN users u ON u.id = p.seller_id
47
49
  WHERE p.category IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
48
50
  ORDER BY sales_count DESC LIMIT 10
49
- `).all(...wishCats, ...exclArgs);
51
+ `, [...wishCats, ...exclArgs]);
50
52
  }
51
53
  let pastSellerProducts = [];
52
54
  const pastSellers = [...knownSellerIds].filter(s => !followedSellerIds.includes(s));
53
55
  if (pastSellers.length > 0) {
54
56
  const ph = pastSellers.map(() => '?').join(',');
55
- pastSellerProducts = db.prepare(`
57
+ pastSellerProducts = await dbAll(`
56
58
  SELECT ${baseCols}
57
59
  FROM products p JOIN users u ON u.id = p.seller_id
58
60
  WHERE p.seller_id IN (${ph}) AND p.status = 'active' AND p.stock > 0 ${exclSql}
59
61
  ORDER BY p.created_at DESC LIMIT 10
60
- `).all(...pastSellers, ...exclArgs);
62
+ `, [...pastSellers, ...exclArgs]);
61
63
  }
62
- const fallback = db.prepare(`
64
+ const fallback = await dbAll(`
63
65
  SELECT ${baseCols}
64
66
  FROM products p JOIN users u ON u.id = p.seller_id
65
67
  WHERE p.status = 'active' AND p.stock > 0 ${exclSql}
66
68
  ORDER BY sales_count DESC, p.created_at DESC LIMIT 10
67
- `).all(...exclArgs);
69
+ `, exclArgs);
68
70
  const seen = new Set();
69
71
  const labeled = (bucket, arr) => arr.filter(it => {
70
72
  const id = String(it.id);
@@ -88,7 +90,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
88
90
  },
89
91
  });
90
92
  });
91
- app.get('/api/feed', (req, res) => {
93
+ app.get('/api/feed', async (req, res) => {
92
94
  const user = auth(req, res);
93
95
  if (!user)
94
96
  return;
@@ -137,13 +139,13 @@ export function registerBuyerFeedsRoutes(app, deps) {
137
139
  WHERE ts IS NOT NULL
138
140
  ORDER BY ts DESC LIMIT 50
139
141
  `;
140
- const events = db.prepare(sql).all(...params);
142
+ const events = await dbAll(sql, params);
141
143
  res.json({ events, scope });
142
144
  });
143
145
  // 雷达扫描 MVP (2026-05-29):scope 范围档 + window 时间窗,k≥3 守护贯穿
144
146
  // scope: cell(本格) / neighbors(周边 3×3) / region(同城) / global(全网)
145
147
  // window: 24h / 7d / 30d
146
- app.get('/api/nearby', (req, res) => {
148
+ app.get('/api/nearby', async (req, res) => {
147
149
  const user = auth(req, res);
148
150
  if (!user)
149
151
  return;
@@ -153,7 +155,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
153
155
  const days = windowKey === '24h' ? 1 : windowKey === '30d' ? 30 : 7;
154
156
  const { precision_deg, approx_km } = getNearbyCellPrecision();
155
157
  const K = getProtocolParam('nearby_k_anonymity', 3);
156
- const u = db.prepare("SELECT geo_lat, geo_lng, geo_updated_at, region FROM users WHERE id = ?").get(user.id);
158
+ const u = (await dbOne("SELECT geo_lat, geo_lng, geo_updated_at, region FROM users WHERE id = ?", [user.id]));
157
159
  const needsGeo = scope === 'cell' || scope === 'neighbors';
158
160
  if (needsGeo && (u?.geo_lat == null || u?.geo_lng == null)) {
159
161
  // 本格/周边需定位;同城/全网不需 → 前端可引导切到更大范围
@@ -186,24 +188,24 @@ export function registerBuyerFeedsRoutes(app, deps) {
186
188
  scopeLabel = '全网';
187
189
  }
188
190
  const dayClause = `o.updated_at > datetime('now', '-${days} day')`;
189
- const totals = db.prepare(`
191
+ const totals = (await dbOne(`
190
192
  SELECT COUNT(DISTINCT o.buyer_id) as au, COUNT(*) as orders
191
193
  FROM orders o JOIN users u ON u.id = o.buyer_id
192
194
  WHERE ${where} AND o.status = 'completed' AND ${dayClause}
193
- `).get(...args);
195
+ `, args));
194
196
  const sufficient = Number(totals.au) >= K;
195
- const topProducts = sufficient ? db.prepare(`
197
+ const topProducts = sufficient ? await dbAll(`
196
198
  SELECT p.id, p.title, p.price, p.category, p.images, COUNT(DISTINCT o.buyer_id) as buyers
197
199
  FROM orders o JOIN users u ON u.id = o.buyer_id JOIN products p ON p.id = o.product_id
198
200
  WHERE ${where} AND o.status = 'completed' AND ${dayClause}
199
201
  GROUP BY p.id HAVING buyers >= ? ORDER BY buyers DESC LIMIT 10
200
- `).all(...args, K) : [];
201
- const topCategories = sufficient ? db.prepare(`
202
+ `, [...args, K]) : [];
203
+ const topCategories = sufficient ? await dbAll(`
202
204
  SELECT p.category, COUNT(*) as orders, COUNT(DISTINCT o.buyer_id) as buyers
203
205
  FROM orders o JOIN users u ON u.id = o.buyer_id JOIN products p ON p.id = o.product_id
204
206
  WHERE ${where} AND o.status = 'completed' AND ${dayClause} AND p.category IS NOT NULL
205
207
  GROUP BY p.category HAVING buyers >= ? ORDER BY orders DESC LIMIT 6
206
- `).all(...args, K) : [];
208
+ `, [...args, K]) : [];
207
209
  const staleDays = u?.geo_updated_at
208
210
  ? Math.floor((Date.now() - new Date(u.geo_updated_at.replace(' ', 'T') + 'Z').getTime()) / 86400_000)
209
211
  : null;
@@ -1,12 +1,13 @@
1
1
  import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
2
2
  import { notifyTransition } from '../../layer2-business/L2-6-notifications/notification-engine.js';
3
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
3
4
  export function registerCartRoutes(app, deps) {
4
5
  const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent, checkStockAndMaybeDelist, addHours } = deps;
5
- app.get('/api/cart', (req, res) => {
6
+ app.get('/api/cart', async (req, res) => {
6
7
  const user = auth(req, res);
7
8
  if (!user)
8
9
  return;
9
- const items = db.prepare(`
10
+ const items = await dbAll(`
10
11
  SELECT c.product_id, c.qty, c.added_at,
11
12
  p.title, p.price, p.category, p.commission_rate, p.stock, p.status as product_status,
12
13
  u.name as seller_name
@@ -15,10 +16,10 @@ export function registerCartRoutes(app, deps) {
15
16
  JOIN users u ON u.id = p.seller_id
16
17
  WHERE c.user_id = ?
17
18
  ORDER BY c.added_at DESC
18
- `).all(user.id);
19
+ `, [user.id]);
19
20
  res.json({ items });
20
21
  });
21
- app.post('/api/cart', (req, res) => {
22
+ app.post('/api/cart', async (req, res) => {
22
23
  const user = auth(req, res);
23
24
  if (!user)
24
25
  return;
@@ -26,29 +27,29 @@ export function registerCartRoutes(app, deps) {
26
27
  const q = Math.max(1, Math.min(99, Number(qty) || 1));
27
28
  if (!product_id)
28
29
  return void res.json({ error: 'product_id 必填' });
29
- const product = db.prepare("SELECT id, status FROM products WHERE id = ?").get(product_id);
30
+ const product = await dbOne("SELECT id, status FROM products WHERE id = ?", [product_id]);
30
31
  if (!product)
31
32
  return void res.json({ error: '商品不存在' });
32
33
  if (product.status !== 'active')
33
34
  return void res.json({ error: '商品已下架' });
34
- db.prepare(`
35
+ await dbRun(`
35
36
  INSERT INTO cart_items (user_id, product_id, qty) VALUES (?, ?, ?)
36
37
  ON CONFLICT(user_id, product_id) DO UPDATE SET qty = MIN(99, cart_items.qty + ?)
37
- `).run(user.id, product_id, q, q);
38
+ `, [user.id, product_id, q, q]);
38
39
  res.json({ ok: true });
39
40
  });
40
- app.patch('/api/cart/:product_id', (req, res) => {
41
+ app.patch('/api/cart/:product_id', async (req, res) => {
41
42
  const user = auth(req, res);
42
43
  if (!user)
43
44
  return;
44
45
  const q = Math.max(1, Math.min(99, Number(req.body.qty) || 1));
45
- const r = db.prepare("UPDATE cart_items SET qty = ? WHERE user_id = ? AND product_id = ?").run(q, user.id, req.params.product_id);
46
+ const r = await dbRun("UPDATE cart_items SET qty = ? WHERE user_id = ? AND product_id = ?", [q, user.id, req.params.product_id]);
46
47
  if (r.changes === 0)
47
48
  return void res.json({ error: '购物车中没有此商品' });
48
49
  res.json({ ok: true, qty: q });
49
50
  });
50
51
  // C-1: 购物车批量下单(按 seller 自动分订单)
51
- app.post('/api/cart/checkout', (req, res) => {
52
+ app.post('/api/cart/checkout', async (req, res) => {
52
53
  const user = auth(req, res);
53
54
  if (!user)
54
55
  return;
@@ -59,11 +60,11 @@ export function registerCartRoutes(app, deps) {
59
60
  const { shipping_address, notes } = req.body || {};
60
61
  if (!shipping_address)
61
62
  return void res.status(400).json({ error: '请填写收货地址' });
62
- const items = db.prepare(`
63
+ const items = await dbAll(`
63
64
  SELECT c.product_id, c.qty, p.title, p.price, p.stock, p.seller_id, p.has_variants, p.status
64
65
  FROM cart_items c JOIN products p ON p.id = c.product_id
65
66
  WHERE c.user_id = ?
66
- `).all(user.id);
67
+ `, [user.id]);
67
68
  if (items.length === 0)
68
69
  return void res.status(400).json({ error: '购物车为空' });
69
70
  const skipped = [];
@@ -93,7 +94,7 @@ export function registerCartRoutes(app, deps) {
93
94
  if (ok.length === 0) {
94
95
  return void res.status(400).json({ error: '购物车中无可下单商品', skipped });
95
96
  }
96
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
97
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
97
98
  if (!wallet)
98
99
  return void res.status(500).json({ error: '钱包记录缺失' });
99
100
  if (wallet.balance < totalNeed)
@@ -145,11 +146,11 @@ export function registerCartRoutes(app, deps) {
145
146
  total_paid: created.reduce((s, c) => s + c.total, 0),
146
147
  });
147
148
  });
148
- app.delete('/api/cart/:product_id', (req, res) => {
149
+ app.delete('/api/cart/:product_id', async (req, res) => {
149
150
  const user = auth(req, res);
150
151
  if (!user)
151
152
  return;
152
- db.prepare("DELETE FROM cart_items WHERE user_id = ? AND product_id = ?").run(user.id, req.params.product_id);
153
+ await dbRun("DELETE FROM cart_items WHERE user_id = ? AND product_id = ?", [user.id, req.params.product_id]);
153
154
  res.json({ ok: true });
154
155
  });
155
156
  }