@seasonkoh/webaz 0.1.27 → 0.1.29

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 (121) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-account.js +977 -0
  46. package/dist/pwa/public/app-admin.js +608 -0
  47. package/dist/pwa/public/app-agents.js +63 -0
  48. package/dist/pwa/public/app-ai.js +2162 -0
  49. package/dist/pwa/public/app-chat-poll.js +29 -0
  50. package/dist/pwa/public/app-contribution.js +836 -0
  51. package/dist/pwa/public/app-create-kinds.js +17 -0
  52. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  53. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  54. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  55. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  56. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  57. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  58. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  59. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  60. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  61. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  62. package/dist/pwa/public/app-direct-pay.js +227 -0
  63. package/dist/pwa/public/app-discover.js +1296 -0
  64. package/dist/pwa/public/app-external-links.js +32 -0
  65. package/dist/pwa/public/app-listings.js +226 -0
  66. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  67. package/dist/pwa/public/app-price.js +55 -0
  68. package/dist/pwa/public/app-product-media.js +15 -0
  69. package/dist/pwa/public/app-profile.js +1692 -0
  70. package/dist/pwa/public/app-seller.js +199 -0
  71. package/dist/pwa/public/app-shop.js +1145 -0
  72. package/dist/pwa/public/app.js +13388 -22277
  73. package/dist/pwa/public/i18n.js +413 -196
  74. package/dist/pwa/public/index.html +27 -0
  75. package/dist/pwa/public/openapi.json +579 -1
  76. package/dist/pwa/routes/admin-analytics.js +4 -2
  77. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  78. package/dist/pwa/routes/agent-grants.js +255 -0
  79. package/dist/pwa/routes/buyer-feeds.js +2 -1
  80. package/dist/pwa/routes/chat.js +6 -1
  81. package/dist/pwa/routes/dashboards.js +2 -1
  82. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  83. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  84. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  85. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  86. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  87. package/dist/pwa/routes/fx.js +12 -0
  88. package/dist/pwa/routes/leaderboard.js +47 -9
  89. package/dist/pwa/routes/listings.js +2 -2
  90. package/dist/pwa/routes/logistics.js +4 -0
  91. package/dist/pwa/routes/manifests.js +38 -0
  92. package/dist/pwa/routes/me-data.js +5 -2
  93. package/dist/pwa/routes/orders-action.js +117 -9
  94. package/dist/pwa/routes/orders-create.js +4 -0
  95. package/dist/pwa/routes/orders-read.js +36 -0
  96. package/dist/pwa/routes/p2p-products.js +2 -2
  97. package/dist/pwa/routes/products-create.js +5 -3
  98. package/dist/pwa/routes/products-links.js +34 -0
  99. package/dist/pwa/routes/products-list.js +2 -1
  100. package/dist/pwa/routes/products-update.js +22 -2
  101. package/dist/pwa/routes/promoter.js +3 -0
  102. package/dist/pwa/routes/referral.js +4 -0
  103. package/dist/pwa/routes/returns.js +26 -1
  104. package/dist/pwa/routes/rewards-apply.js +10 -5
  105. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  106. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  107. package/dist/pwa/routes/shops.js +2 -1
  108. package/dist/pwa/routes/url-claim.js +2 -2
  109. package/dist/pwa/routes/users-public.js +8 -0
  110. package/dist/pwa/routes/wallet-read.js +3 -0
  111. package/dist/pwa/routes/webauthn.js +7 -2
  112. package/dist/pwa/server-schema.js +9 -0
  113. package/dist/pwa/server.js +261 -1706
  114. package/dist/runtime/agent-grant-scopes.js +128 -0
  115. package/dist/runtime/agent-grant-verifier.js +67 -0
  116. package/dist/runtime/agent-pairing.js +60 -0
  117. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  118. package/dist/runtime/webaz-schema-helpers.js +1952 -0
  119. package/dist/store-verification.js +77 -0
  120. package/dist/version.js +1 -1
  121. package/package.json +80 -2
@@ -1,7 +1,8 @@
1
1
  import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { invalidateProductVerification } from '../../product-verification.js';
2
3
  export function registerProductsUpdateRoutes(app, deps) {
3
- // db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
4
- const { auth, makeCommitmentHash, makeDescriptionHash, makePriceHash, notifyWaitlist, notifyWishlistPriceDrop, checkStockAndMaybeDelist } = deps;
4
+ // db 已走 RFC-016 异步 seam(dbOne/dbRun);deps.db 仅用于 direct-pay 逐品验证作废(sync helper,反作弊生命周期)
5
+ const { db, auth, makeCommitmentHash, makeDescriptionHash, makePriceHash, notifyWaitlist, notifyWishlistPriceDrop, checkStockAndMaybeDelist } = deps;
5
6
  app.put('/api/products/:id', async (req, res) => {
6
7
  const user = auth(req, res);
7
8
  if (!user)
@@ -134,6 +135,25 @@ export function registerProductsUpdateRoutes(app, deps) {
134
135
  console.error('[wishlist price-drop notify]', e);
135
136
  }
136
137
  }
138
+ // 反作弊(PR-⑥):任何【买家可见的商品身份字段】变更 → 作废该商品的直付逐品验证,强制重新验证,防"先验证商品 A,
139
+ // 再改成商品 B"绕过逐品硬门。external-link 变更在 products-links 路由另行作废。fail-soft,不阻断编辑。
140
+ // ⚠️ 必须含 i18n_titles/i18n_descs:formatProductForAgent 按 Accept-Language 把 title/description 换成对应语言,
141
+ // 只改 en/ja/ko 标题描述同样是"换货"(非中文买家看到的就变了);并含 brand/model(商品身份)。
142
+ const newBrand = brand ?? product.brand;
143
+ const newModel = model ?? product.model;
144
+ const eq = (a, b) => String(a ?? '') === String(b ?? '');
145
+ const materialChanged = !eq(newTitle, product.title) || !eq(newDesc, product.description)
146
+ || Number(newPrice) !== Number(product.price) || !eq(specsJson, product.specs)
147
+ || !eq(newI18nTitles, product.i18n_titles) || !eq(newI18nDescs, product.i18n_descs)
148
+ || !eq(newBrand, product.brand) || !eq(newModel, product.model);
149
+ if (materialChanged) {
150
+ try {
151
+ invalidateProductVerification(db, String(req.params.id));
152
+ }
153
+ catch (e) {
154
+ console.error('[product-verify invalidate]', e);
155
+ }
156
+ }
137
157
  res.json({ success: true });
138
158
  });
139
159
  }
@@ -28,6 +28,8 @@ export function registerPromoterRoutes(app, deps) {
28
28
  for (const r of earned)
29
29
  byLevel[r.level] = { orders: r.orders, total: r.total };
30
30
  const grand = byLevel[1].total + byLevel[2].total + byLevel[3].total;
31
+ // RFC-018: commission accrued but still in the clearing window (matures into grand_total). Pure read.
32
+ const clearing = (await dbOne("SELECT COALESCE(SUM(amount),0) as s FROM pending_commission_escrow WHERE recipient_user_id = ? AND matures_at IS NOT NULL AND status = 'pending'", [userId])).s;
31
33
  const recent = await dbAll(`
32
34
  SELECT cr.id, cr.order_id, cr.level, cr.amount, cr.rate, cr.created_at,
33
35
  u.name as source_buyer_name
@@ -142,6 +144,7 @@ export function registerPromoterRoutes(app, deps) {
142
144
  team: { l1, l2, l3, total: l1 + l2 + l3 },
143
145
  earnings: {
144
146
  grand_total: grand,
147
+ clearing_total: clearing, // RFC-018: accrued, maturing after the return window (not yet paid)
145
148
  l1: byLevel[1],
146
149
  l2: byLevel[2],
147
150
  l3: byLevel[3],
@@ -23,6 +23,9 @@ export function registerReferralRoutes(app, deps) {
23
23
  `, [user.id]));
24
24
  const todayEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`, [user.id])).t;
25
25
  const monthEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`, [user.id])).t;
26
+ // RFC-018: commission accrued but still in the clearing window (pending → matures into total_waz).
27
+ // Pure read; surfaced so earnings don't appear to vanish during clearing (Option A keeps total_waz = paid).
28
+ const clearingWaz = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM pending_commission_escrow WHERE recipient_user_id = ? AND matures_at IS NOT NULL AND status = 'pending'`, [user.id])).t;
26
29
  res.json({
27
30
  invite_code: code,
28
31
  invite_link: code ? `${req.protocol}://${req.get('host')}/i/${code}` : null,
@@ -34,6 +37,7 @@ export function registerReferralRoutes(app, deps) {
34
37
  total_waz: earnings.total,
35
38
  today_waz: todayEarnings,
36
39
  month_waz: monthEarnings,
40
+ clearing_waz: clearingWaz, // RFC-018: accrued, maturing after the return window (not yet paid)
37
41
  },
38
42
  });
39
43
  });
@@ -30,6 +30,26 @@ export function registerReturnsRoutes(app, deps) {
30
30
  if (debited.changes !== 1)
31
31
  throw new Error('INSUFFICIENT_SELLER_BALANCE');
32
32
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refundAmt, rr.buyer_id);
33
+ // RFC-018: reverse this order's in-window clearing commission, proportional to the refund.
34
+ // These pending rows were never paid (maturation is gated on matures_at), so this is a pure
35
+ // reversal — no clawback. matures_at IS NOT NULL targets clearing rows only; opt-out escrow
36
+ // rows (matures_at IS NULL) are untouched. Atomic with the refund.
37
+ const ordTotal = Number(db.prepare('SELECT total_amount FROM orders WHERE id = ?').get(rr.order_id)?.total_amount ?? 0);
38
+ const refundFrac = ordTotal > 0 ? Math.min(1, refundAmt / ordTotal) : 1;
39
+ if (refundFrac >= 1) {
40
+ db.prepare(`UPDATE pending_commission_escrow SET status='reversed' WHERE order_id = ? AND status='pending' AND matures_at IS NOT NULL`).run(rr.order_id);
41
+ // RFC-018 PR4: a single FULL return is no longer a genuine sale — decrement the stored product
42
+ // completion_count (incremented at settleOrder). Idempotent: the return CAS'd to 'refunded'
43
+ // exactly once above, so this runs once. Partial refund stays a genuine sale (no decrement).
44
+ // Edge: multiple partial refunds cumulatively reaching full aren't caught here (each refundFrac<1),
45
+ // so the stored counter may transiently over-count by 1 — but the AUTHORITATIVE genuine count
46
+ // (genuineSalePredicate, cumulative SUM) is correct everywhere on-read, and the completion_count
47
+ // backfill (genuine-aware) self-heals the stored counter. completion_count is best-effort social proof.
48
+ db.prepare(`UPDATE products SET completion_count = MAX(0, COALESCE(completion_count,0) - 1) WHERE id = ?`).run(rr.product_id);
49
+ }
50
+ else {
51
+ db.prepare(`UPDATE pending_commission_escrow SET amount = amount * ? WHERE order_id = ? AND status='pending' AND matures_at IS NOT NULL`).run(1 - refundFrac, rr.order_id);
52
+ }
33
53
  // 3. 恢复库存(CAS + 扣款成功后)
34
54
  const ord = db.prepare('SELECT quantity, source, variant_id FROM orders WHERE id = ?').get(rr.order_id);
35
55
  if (ord && ord.source !== 'secondhand') {
@@ -69,7 +89,7 @@ export function registerReturnsRoutes(app, deps) {
69
89
  return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
70
90
  const order = await dbOne(`
71
91
  SELECT o.id, o.buyer_id, o.seller_id, o.product_id, o.status, o.total_amount, o.created_at, o.updated_at,
72
- p.return_days, p.title as product_title
92
+ o.payment_rail, p.return_days, p.title as product_title
73
93
  FROM orders o JOIN products p ON p.id = o.product_id
74
94
  WHERE o.id = ?
75
95
  `, [req.params.order_id]);
@@ -77,6 +97,11 @@ export function registerReturnsRoutes(app, deps) {
77
97
  return void res.status(404).json({ error: '订单不存在' });
78
98
  if (order.buyer_id !== user.id)
79
99
  return void res.status(403).json({ error: '仅买家可申请退货' });
100
+ // 直付(Rail1)= 非托管、refund:none —— WebAZ 不持货款,退款经现有 escrow 钱包路径会错误移动无关 WAZ。
101
+ // 故 direct_p2p 不走退货/退款流程(买家不满走争议=信誉处理);平台费应收的冲销由治理/admin 调整处理(非此路径)。
102
+ if (order.payment_rail === 'direct_p2p') {
103
+ return void res.status(400).json({ error: '直付(非托管)订单不支持退款流程;如有问题请发起争议', error_code: 'DIRECT_PAY_NO_REFUND' });
104
+ }
80
105
  // P0-1: 只允许 completed 退货 — escrow 已结算
81
106
  if (order.status !== 'completed') {
82
107
  return void res.status(400).json({ error: '仅订单完成后可申请退货(确认收货后)' });
@@ -37,8 +37,10 @@ export function registerRewardsApplyRoutes(app, deps) {
37
37
  missing.push(`completed_orders ${completedOrders}/${minOrders}`);
38
38
  if (requirePasskey === 1 && passkeyCount === 0)
39
39
  missing.push('passkey_not_registered');
40
- const pending = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending'", [userId]));
41
- const expired = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired'", [userId]));
40
+ // RFC-018: matures_at IS NULL = opt-out escrow (claimable on activation). Clearing rows
41
+ // (matures_at NOT NULL) auto-mature via rewards-clearing-mature and must NOT be drained here.
42
+ const pending = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NULL", [userId]));
43
+ const expired = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired' AND matures_at IS NULL", [userId]));
42
44
  res.json({
43
45
  state,
44
46
  opted_in: optIn === 1,
@@ -138,12 +140,15 @@ export function registerRewardsApplyRoutes(app, deps) {
138
140
  VALUES (?, 'activate', ?, ?, ?, ?, ?, ?, ?)`)
139
141
  .run(userId, consent_version, currentMajor.hash, webauthn_token || null, // store gate_token id as audit cross-ref to webauthn_gate_tokens
140
142
  requirePasskey === 1 ? 'passkey' : 'password', sha256_hex(req.ip || '').slice(0, 16), sha256_hex(String(req.headers['user-agent'] || '')).slice(0, 16), now);
141
- // Activate batch settle: drain pending escrow to wallet
143
+ // Activate batch settle: drain OPT-OUT escrow to wallet. RFC-018: matures_at IS NULL excludes
144
+ // clearing rows — those are NOT claimable on opt-in; they auto-mature after the return window
145
+ // (rewards-clearing-mature). Draining them here would pay commission early and defeat the
146
+ // clearing window's reversal guarantee (a returned order could no longer be reversed → clawback).
142
147
  const pending = db.prepare(`SELECT id, amount, attribution_path FROM pending_commission_escrow
143
- WHERE recipient_user_id = ? AND status = 'pending' AND expires_at > ?`).all(userId, now);
148
+ WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NULL AND expires_at > ?`).all(userId, now);
144
149
  let total = 0;
145
150
  for (const p of pending) {
146
- const upd = db.prepare(`UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending'`).run(now, p.id);
151
+ const upd = db.prepare(`UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending' AND matures_at IS NULL`).run(now, p.id);
147
152
  if (upd.changes === 0)
148
153
  continue; // race: expire cron took it
149
154
  db.prepare(`UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?`).run(p.amount, p.amount, userId);
@@ -0,0 +1,96 @@
1
+ // RFC-016 Phase 1 — sweep read → async seam; the per-row pay tx stays sync (Phase 3 → pg).
2
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
3
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
4
+ import { applyWalletDelta } from '../../ledger.js';
5
+ import { toUnits, toDecimal } from '../../money.js';
6
+ // #7 Commission source_type (moved from settleCommission). Pure read — stamps commission_records.source_type.
7
+ function commissionSourceType(db, productId, uid) {
8
+ if (!uid)
9
+ return 'sponsor';
10
+ const attr = db.prepare(`SELECT shareable_id FROM product_share_attribution WHERE product_id = ? AND sharer_id = ? AND shareable_id IS NOT NULL ORDER BY created_at DESC LIMIT 1`).get(productId, uid);
11
+ if (!attr)
12
+ return 'sponsor';
13
+ const sh = db.prepare(`SELECT type FROM shareables WHERE id = ?`).get(attr.shareable_id);
14
+ if (!sh)
15
+ return 'sponsor';
16
+ return sh.type === 'note' ? 'note' : 'link';
17
+ }
18
+ /**
19
+ * Mature ONE clearing commission row. Re-validates genuine closure (completed + no open dispute), then
20
+ * writes commission_records + credits the wallet in one tx. CAS on status='pending' → idempotent; races
21
+ * and re-runs lose. Holds (no write) while a dispute is open. Exported for direct unit testing.
22
+ */
23
+ export function matureClearingRow(db, row) {
24
+ let outcome = 'skipped';
25
+ db.transaction(() => {
26
+ const order = db.prepare("SELECT buyer_id, product_id, snapshot_commission_rate, buyer_region, status FROM orders WHERE id = ?").get(row.order_id);
27
+ if (!order || order.status !== 'completed') {
28
+ outcome = 'held';
29
+ return;
30
+ } // completed is terminal; defensive hold
31
+ const openDispute = db.prepare("SELECT 1 FROM disputes WHERE order_id = ? AND status NOT IN ('resolved','dismissed','closed') LIMIT 1").get(row.order_id);
32
+ if (openDispute) {
33
+ outcome = 'held';
34
+ return;
35
+ } // hold while unresolved (e.g. return escalated to dispute)
36
+ const claimed = db.prepare("UPDATE pending_commission_escrow SET status='settled', settled_at=? WHERE id=? AND status='pending' AND matures_at IS NOT NULL").run(Date.now(), row.id);
37
+ if (claimed.changes !== 1) {
38
+ outcome = 'skipped';
39
+ return;
40
+ } // race lost / already settled / reversed
41
+ const amountU = toUnits(Number(row.amount));
42
+ if (amountU > 0) {
43
+ const level = Number(String(row.attribution_path).replace(/^L/, '')) || 0;
44
+ const rate = Number(order.snapshot_commission_rate ?? 0.10);
45
+ let region = order.buyer_region || null;
46
+ if (!region)
47
+ region = db.prepare("SELECT region FROM users WHERE id = ?").get(order.buyer_id)?.region ?? 'global';
48
+ const srcType = commissionSourceType(db, order.product_id, row.recipient_user_id);
49
+ try {
50
+ db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type) VALUES (?,?,?,?,?,?,?,?,?,?)`)
51
+ .run(generateId('comm'), row.order_id, row.recipient_user_id, order.buyer_id, level, toDecimal(amountU), rate, region, 'static', srcType);
52
+ }
53
+ catch (e) { /* UNIQUE — already recorded on a prior partial run */ }
54
+ applyWalletDelta(db, row.recipient_user_id, { balance: amountU, earned: amountU });
55
+ }
56
+ outcome = 'settled';
57
+ })();
58
+ return outcome;
59
+ }
60
+ export async function runClearingMatureSweep(deps) {
61
+ const { db } = deps;
62
+ const now = Date.now();
63
+ const rows = await dbAll(`
64
+ SELECT id, recipient_user_id, order_id, amount, attribution_path
65
+ FROM pending_commission_escrow
66
+ WHERE status = 'pending' AND matures_at IS NOT NULL AND matures_at <= ?
67
+ ORDER BY matures_at ASC
68
+ LIMIT 1000
69
+ `, [now]);
70
+ let settled = 0, held = 0, skipped = 0;
71
+ for (const r of rows) {
72
+ const o = matureClearingRow(db, r);
73
+ if (o === 'settled')
74
+ settled++;
75
+ else if (o === 'held')
76
+ held++;
77
+ else
78
+ skipped++;
79
+ }
80
+ return { scanned: rows.length, settled, held, skipped };
81
+ }
82
+ export function startClearingMatureCron(deps) {
83
+ const ms = 60 * 60 * 1000; // 1h — clearing window granularity is days
84
+ setInterval(async () => {
85
+ try {
86
+ const r = await runClearingMatureSweep(deps);
87
+ if (r.settled > 0 || r.held > 0) {
88
+ console.log(`[clearing-mature] scanned ${r.scanned}, settled ${r.settled}, held ${r.held}, skipped ${r.skipped}`);
89
+ }
90
+ }
91
+ catch (e) {
92
+ console.error('[clearing-mature-cron]', e);
93
+ }
94
+ }, ms);
95
+ console.log('⏳ RFC-018 commission clearing maturation cron 已启动 (每 1h, anchor=matures_at)');
96
+ }
@@ -3,17 +3,21 @@ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js';
3
3
  export async function runEscrowExpireSweep(deps) {
4
4
  const { db, redirectToCommissionReserve } = deps;
5
5
  const now = Date.now();
6
+ // RFC-018: pending_commission_escrow now holds TWO lifecycles, discriminated by matures_at:
7
+ // - matures_at IS NULL → opt-out escrow (this cron: expire → commission_reserve / pool).
8
+ // - matures_at NOT NULL → clearing rows (the SEPARATE rewards-clearing-mature cron: pay/reverse).
9
+ // We MUST exclude clearing rows here, or they'd be wrongly expired to the pool instead of paid.
6
10
  const rows = await dbAll(`
7
11
  SELECT id, recipient_user_id, order_id, amount, attribution_path, expires_at
8
12
  FROM pending_commission_escrow
9
- WHERE status = 'pending' AND expires_at <= ?
13
+ WHERE status = 'pending' AND matures_at IS NULL AND expires_at <= ?
10
14
  ORDER BY expires_at ASC
11
15
  LIMIT 1000
12
16
  `, [now]);
13
17
  const expired = [];
14
18
  for (const r of rows) {
15
19
  db.transaction(() => {
16
- const upd = db.prepare(`UPDATE pending_commission_escrow SET status='expired', expired_to_charity_at=? WHERE id=? AND status='pending'`).run(now, r.id);
20
+ const upd = db.prepare(`UPDATE pending_commission_escrow SET status='expired', expired_to_charity_at=? WHERE id=? AND status='pending' AND matures_at IS NULL`).run(now, r.id);
17
21
  if (upd.changes === 0)
18
22
  return; // race lost — another sweep already took it
19
23
  if (r.attribution_path === 'pv_pair') {
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerShopsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
5
  const { auth } = deps;
@@ -20,7 +21,7 @@ export function registerShopsRoutes(app, deps) {
20
21
  const sellerId = String(seller.id);
21
22
  const products = await dbAll(`
22
23
  SELECT p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.commission_rate,
23
- (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
24
+ (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count
24
25
  FROM products p
25
26
  WHERE p.seller_id = ? AND p.status = 'active'
26
27
  ORDER BY sales_count DESC, p.created_at DESC
@@ -119,8 +119,8 @@ export function registerUrlClaimRoutes(app, deps) {
119
119
  db.prepare(`INSERT INTO products (
120
120
  id, seller_id, title, description, price, stock, category, stake_amount,
121
121
  specs, source_url, handling_hours, return_days, warranty_days,
122
- commitment_hash, description_hash, price_hash, hashed_at, status, stake_locked_at
123
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse',datetime('now'))`)
122
+ commitment_hash, description_hash, price_hash, hashed_at, status, stake_locked_at, currency
123
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse',datetime('now'),'WAZ')`)
124
124
  .run(productId, user.id, title, description, priceNum, Number(stock), category, stakeAmount, specsJson, url, Number(handling_hours), Number(return_days), Number(warranty_days), makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }), makePriceHash(priceNum, now), now);
125
125
  db.prepare(`INSERT INTO product_external_links
126
126
  (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
@@ -25,6 +25,14 @@ export function registerUsersPublicRoutes(app, deps) {
25
25
  return;
26
26
  userId = user.id;
27
27
  }
28
+ else {
29
+ // ref 三态(usr_xxx / permanent_code / @handle)必须解析为 canonical id;
30
+ // 漏掉解析会让 @handle/permanent_code 当字面 user_id 去查 → 永远落到默认 'new'。
31
+ const resolved = await resolveUserId(userId);
32
+ if (!resolved)
33
+ return void res.status(404).json({ error: 'user not found' });
34
+ userId = resolved;
35
+ }
28
36
  const row = await dbOne(`
29
37
  SELECT level, MAX(trust_score) as max_score
30
38
  FROM agent_reputation WHERE user_id = ?
@@ -181,8 +181,11 @@ export function registerWalletReadRoutes(app, deps) {
181
181
  FROM orders WHERE seller_id = ? AND status = 'completed'
182
182
  `, [user.id]));
183
183
  const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total + Number(sales.total);
184
+ // RFC-018: commission accrued but still in the clearing window (matures into commissions/total_income). Pure read.
185
+ const clearing = (await dbOne("SELECT COALESCE(SUM(amount),0) as s FROM pending_commission_escrow WHERE recipient_user_id = ? AND matures_at IS NOT NULL AND status = 'pending'", [user.id])).s;
184
186
  res.json({
185
187
  commissions: commMap,
188
+ commission_clearing: Number(clearing.toFixed(2)), // RFC-018: accrued, maturing after the return window (not yet paid)
186
189
  sales: { count: sales.cnt, total: Number(Number(sales.total).toFixed(2)) },
187
190
  total_income: Number(totalIncome.toFixed(2)),
188
191
  });
@@ -3,8 +3,9 @@ import { randomBytes } from 'node:crypto';
3
3
  // RFC-004 体验补:绑定 Passkey 后,追溯补发此前"已受理但无锚点跳过"的建设信誉。
4
4
  import { grantPendingAnchorCredits } from '../../layer2-business/L2-8-feedback/build-feedback-engine.js';
5
5
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
6
+ import { registerAgentGrantsRoutes } from './agent-grants.js'; // RFC-020 PR-B — Passkey-domain delegation grants (keeps server.ts untouched)
6
7
  export function registerWebauthnRoutes(app, deps) {
7
- const { db, auth, generateId, rpId, rpName, origin, challengeTtlMs, gateTtlMs, invalidateAgentRiskCacheForUser, requireHumanPresence } = deps;
8
+ const { db, auth, generateId, rateLimitOk, rpId, rpName, origin, challengeTtlMs, gateTtlMs, invalidateAgentRiskCacheForUser, requireHumanPresence } = deps;
8
9
  // 1. 注册:start — 生成 challenge + 选项
9
10
  app.post('/api/webauthn/register/start', async (req, res) => {
10
11
  const user = auth(req, res);
@@ -73,7 +74,7 @@ export function registerWebauthnRoutes(app, deps) {
73
74
  if (!user)
74
75
  return;
75
76
  const purpose = String(req.body?.purpose || '').trim();
76
- const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink']);
77
+ const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink', 'direct_pay_disclosure_ack', 'direct_pay_order_action', 'direct_receive_production_confirm', 'direct_pay_aml_review', 'direct_pay_kyb_ingress', 'direct_pay_sanctions_ingress', 'direct_pay_aml_ingress', 'direct_pay_admin_readiness', 'direct_pay_deferral_approve', 'direct_pay_deferral_reject', 'direct_pay_product_verify', 'direct_pay_store_verify', 'direct_pay_fee_prepay_record', 'direct_pay_fee_adjust', 'direct_pay_fee_refund', 'direct_receive_account_manage']);
77
78
  if (!allowed.has(purpose))
78
79
  return void res.status(400).json({ error: 'invalid purpose' });
79
80
  const purpose_data = req.body?.purpose_data ?? null;
@@ -192,4 +193,8 @@ export function registerWebauthnRoutes(app, deps) {
192
193
  await dbRun('UPDATE users SET webauthn_required_for_withdraw = ? WHERE id = ?', [required, user.id]);
193
194
  res.json({ success: true, required_for_withdraw: !!required });
194
195
  });
196
+ // RFC-020 PR-B — agent delegation grants (issue/read/revoke). Co-registered with the
197
+ // Passkey security routes so the money-dense server.ts stays untouched. Safe scopes
198
+ // only; risk scopes default-hard-reject. Reuses db/auth/generateId from WebauthnDeps.
199
+ registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk });
195
200
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compatibility re-export.
3
+ *
4
+ * The pure idempotent schema helpers were relocated to a neutral top-level
5
+ * module (src/runtime/webaz-schema-helpers.ts) so the PWA boot path and the MCP
6
+ * runtime schema composition root can share ONE source. This file keeps the
7
+ * historical `./server-schema.js` import path working for src/pwa/server.ts.
8
+ */
9
+ export * from '../runtime/webaz-schema-helpers.js';