@seasonkoh/webaz 0.1.8 → 0.1.9

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 (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3543 -852
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31230 -2345
  22. package/dist/pwa/public/i18n.js +5282 -111
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +4 -1
  25. package/dist/pwa/public/manifest.json +39 -4
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +278 -5
  28. package/dist/pwa/public/sw.js +41 -2
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9247 -2097
  153. package/package.json +8 -3
@@ -0,0 +1,178 @@
1
+ export function registerP2pProductsRoutes(app, deps) {
2
+ const { db, auth, generateId, verifyP2pSig, isValidPeerEndpoint, isFreshSignedAt, P2P_TITLE_MAX, P2P_THUMB_MAX, P2P_DAILY_CAP, RFQ_MAX_PRICE, RFQ_MAX_QTY } = deps;
3
+ // 发布 / 重发 P2P 商品
4
+ app.post('/api/p2p-products', (req, res) => {
5
+ const user = auth(req, res);
6
+ if (!user)
7
+ return;
8
+ if (user.role !== 'seller')
9
+ return void res.json({ error: '仅卖家可上架' });
10
+ const body = req.body;
11
+ const title = String(body.title || '').trim();
12
+ if (title.length < 2 || title.length > P2P_TITLE_MAX)
13
+ return void res.json({ error: `title 需 2-${P2P_TITLE_MAX} 字` });
14
+ const price = Number(body.price);
15
+ if (!Number.isFinite(price) || price <= 0)
16
+ return void res.json({ error: 'price 必须 > 0' });
17
+ if (price > RFQ_MAX_PRICE)
18
+ return void res.json({ error: `price 超出上限 ${RFQ_MAX_PRICE} WAZ` });
19
+ const stock = Math.max(1, Math.floor(Number(body.stock) || 1));
20
+ if (stock > RFQ_MAX_QTY)
21
+ return void res.json({ error: `stock 超出上限 ${RFQ_MAX_QTY}` });
22
+ const contentHash = String(body.content_hash || '');
23
+ if (!/^[a-f0-9]{64}$/.test(contentHash))
24
+ return void res.json({ error: 'content_hash 必须为 64 字符十六进制(sha256)' });
25
+ const signedAt = String(body.content_signed_at || '');
26
+ if (!signedAt)
27
+ return void res.json({ error: 'content_signed_at 必填' });
28
+ if (!isFreshSignedAt(signedAt))
29
+ return void res.json({ error: 'content_signed_at 必须在最近 24h 内(防重放)' });
30
+ const signature = String(body.content_signature || '');
31
+ // 用调用者 api_key 验签(防伪造)
32
+ const apiKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
33
+ if (!verifyP2pSig(contentHash, signedAt, apiKey, signature))
34
+ return void res.json({ error: 'content_signature 签名无效' });
35
+ const peerEndpoint = String(body.peer_endpoint || '').trim();
36
+ if (!isValidPeerEndpoint(peerEndpoint))
37
+ return void res.json({ error: 'peer_endpoint 必须是 http:// 或 https:// 协议' });
38
+ // peer_endpoint 可空(manifest_uri 模式留 P2 阶段扩展),但 thumbnail 必须有以供预览
39
+ const thumbnail = body.thumbnail_uri ? String(body.thumbnail_uri) : null;
40
+ if (thumbnail && thumbnail.length > P2P_THUMB_MAX)
41
+ return void res.json({ error: `thumbnail 超过 ${P2P_THUMB_MAX} 字节` });
42
+ // 频率限制
43
+ const today = db.prepare("SELECT COUNT(1) as n FROM products WHERE seller_id = ? AND p2p_mode = 1 AND created_at > datetime('now','-1 day')").get(user.id).n;
44
+ if (today >= P2P_DAILY_CAP)
45
+ return void res.json({ error: `今日 P2P 上架已达上限 ${P2P_DAILY_CAP}` });
46
+ const category = String(body.category || 'general');
47
+ const region = String(body.region || user.region || '全国');
48
+ const id = generateId('p');
49
+ db.prepare(`
50
+ INSERT INTO products (id, seller_id, title, description, price, stock, status, images, ship_regions,
51
+ handling_hours, commission_rate, category_id, stake_amount, p2p_mode, content_hash, peer_endpoint,
52
+ content_signature, content_signed_at)
53
+ VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1,?,?,?,?)
54
+ `).run(id, user.id, title, `[P2P] ${title}(完整详情见卖家节点)`, price, stock, thumbnail ? JSON.stringify([thumbnail]) : '[]', region, contentHash, peerEndpoint || null, signature, signedAt);
55
+ res.json({ id, content_hash: contentHash });
56
+ });
57
+ // 更新(重发 hash + signature,价格/库存/标题可改;旧 hash 给在途订单保留)
58
+ app.patch('/api/p2p-products/:id', (req, res) => {
59
+ const user = auth(req, res);
60
+ if (!user)
61
+ return;
62
+ const product = db.prepare("SELECT * FROM products WHERE id = ? AND p2p_mode = 1").get(req.params.id);
63
+ if (!product)
64
+ return void res.status(404).json({ error: 'P2P 商品不存在' });
65
+ if (product.seller_id !== user.id)
66
+ return void res.status(403).json({ error: '仅卖家本人可修改' });
67
+ if (product.status === 'deleted')
68
+ return void res.json({ error: '已删除商品不可修改' });
69
+ const body = req.body;
70
+ const updates = [];
71
+ const args = [];
72
+ if (body.title != null) {
73
+ const ttl = String(body.title).trim();
74
+ if (ttl.length < 2 || ttl.length > P2P_TITLE_MAX)
75
+ return void res.json({ error: 'title 长度无效' });
76
+ updates.push('title = ?');
77
+ args.push(ttl);
78
+ }
79
+ if (body.price != null) {
80
+ const p = Number(body.price);
81
+ if (!Number.isFinite(p) || p <= 0)
82
+ return void res.json({ error: 'price 无效' });
83
+ if (p > RFQ_MAX_PRICE)
84
+ return void res.json({ error: `price 超上限 ${RFQ_MAX_PRICE}` });
85
+ updates.push('price = ?');
86
+ args.push(p);
87
+ }
88
+ if (body.stock != null) {
89
+ const s = Math.max(0, Math.floor(Number(body.stock) || 0));
90
+ updates.push('stock = ?');
91
+ args.push(s);
92
+ }
93
+ if (body.peer_endpoint !== undefined) {
94
+ const ep = body.peer_endpoint ? String(body.peer_endpoint).trim() : '';
95
+ if (ep && !isValidPeerEndpoint(ep))
96
+ return void res.json({ error: 'peer_endpoint 必须是 http:// 或 https://' });
97
+ updates.push('peer_endpoint = ?');
98
+ args.push(ep || null);
99
+ }
100
+ if (body.status != null) {
101
+ const st = String(body.status);
102
+ if (!['active', 'paused', 'warehouse'].includes(st))
103
+ return void res.json({ error: 'status 无效(active/paused/warehouse)' });
104
+ updates.push('status = ?');
105
+ args.push(st);
106
+ }
107
+ // 富内容变了 → 必须重签 hash
108
+ if (body.content_hash != null || body.content_signature != null) {
109
+ const newHash = String(body.content_hash || '');
110
+ if (!/^[a-f0-9]{64}$/.test(newHash))
111
+ return void res.json({ error: 'content_hash 必须为 sha256 hex' });
112
+ const newSignedAt = String(body.content_signed_at || '');
113
+ if (!isFreshSignedAt(newSignedAt))
114
+ return void res.json({ error: 'content_signed_at 必须在最近 24h 内' });
115
+ const newSig = String(body.content_signature || '');
116
+ const apiKey = req.headers.authorization?.replace('Bearer ', '') ?? '';
117
+ if (!verifyP2pSig(newHash, newSignedAt, apiKey, newSig))
118
+ return void res.json({ error: '新 signature 无效' });
119
+ updates.push('content_hash = ?');
120
+ args.push(newHash);
121
+ updates.push('content_signature = ?');
122
+ args.push(newSig);
123
+ updates.push('content_signed_at = ?');
124
+ args.push(newSignedAt);
125
+ // 旧 hash 自动保留在 orders.content_hash_at_order(争议时凭买家所见 hash 判定)
126
+ }
127
+ if (!updates.length)
128
+ return void res.json({ error: '无任何修改' });
129
+ updates.push("updated_at = datetime('now')");
130
+ args.push(req.params.id);
131
+ db.prepare(`UPDATE products SET ${updates.join(', ')} WHERE id = ?`).run(...args);
132
+ res.json({ success: true });
133
+ });
134
+ // 下架(保留行 + status='warehouse',在途订单 hash 仍可证)
135
+ app.delete('/api/p2p-products/:id', (req, res) => {
136
+ const user = auth(req, res);
137
+ if (!user)
138
+ return;
139
+ const product = db.prepare("SELECT seller_id, status FROM products WHERE id = ? AND p2p_mode = 1").get(req.params.id);
140
+ if (!product)
141
+ return void res.status(404).json({ error: 'P2P 商品不存在' });
142
+ if (product.seller_id !== user.id)
143
+ return void res.status(403).json({ error: '仅卖家本人可下架' });
144
+ const pendingOrders = db.prepare("SELECT COUNT(1) as n FROM orders WHERE product_id = ? AND status NOT IN ('completed','cancelled','refunded','expired')").get(req.params.id).n;
145
+ if (pendingOrders > 0)
146
+ return void res.json({ error: `该商品有 ${pendingOrders} 个进行中订单,无法下架` });
147
+ db.prepare("UPDATE products SET status = 'warehouse', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
148
+ res.json({ success: true });
149
+ });
150
+ // 公开:列表
151
+ app.get('/api/p2p-products', (_req, res) => {
152
+ const rows = db.prepare(`
153
+ SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.images as thumbnail_json,
154
+ p.ship_regions as region, p.content_hash, p.peer_endpoint, p.content_signed_at,
155
+ u.handle as seller_handle, u.region as seller_region
156
+ FROM products p
157
+ LEFT JOIN users u ON u.id = p.seller_id
158
+ WHERE p.status = 'active' AND p.stock > 0 AND p.p2p_mode = 1
159
+ ORDER BY p.created_at DESC
160
+ LIMIT 50
161
+ `).all();
162
+ res.json({ items: rows });
163
+ });
164
+ // 公开:详情(含 hash + peer_endpoint)
165
+ app.get('/api/p2p-products/:id', (req, res) => {
166
+ const row = db.prepare(`
167
+ SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.images as thumbnail_json,
168
+ p.ship_regions as region, p.content_hash, p.peer_endpoint, p.content_signature, p.content_signed_at,
169
+ u.handle as seller_handle, u.region as seller_region, u.permanent_code as seller_code
170
+ FROM products p
171
+ LEFT JOIN users u ON u.id = p.seller_id
172
+ WHERE p.id = ? AND p.p2p_mode = 1
173
+ `).get(req.params.id);
174
+ if (!row)
175
+ return void res.status(404).json({ error: 'P2P 商品不存在' });
176
+ res.json({ product: row });
177
+ });
178
+ }
@@ -0,0 +1,311 @@
1
+ const PAYMENT_METHOD_KINDS = new Set(['crypto_onchain', 'bank_wire', 'card', 'mobile_wallet', 'p2p']);
2
+ const PAYMENT_METHOD_STATUSES = new Set(['active', 'preview', 'inactive', 'deprecated']);
3
+ const RPM_DIRECTIONS = new Set(['deposit', 'withdraw', 'both']);
4
+ const RPM_STATUSES = new Set(['active', 'paused', 'blocked']);
5
+ export function registerPaymentsGovernanceRoutes(app, deps) {
6
+ const { db, generateId, requireRootAdmin } = deps;
7
+ // 写支付变更审计日志
8
+ function logPaymentChange(entity_kind, entity_id, action, oldValue, newValue, changed_by, reason) {
9
+ db.prepare(`INSERT INTO payment_methods_log (entity_kind, entity_id, action, old_value, new_value, changed_by, reason) VALUES (?,?,?,?,?,?,?)`).run(entity_kind, entity_id, action, oldValue == null ? null : JSON.stringify(oldValue), newValue == null ? null : JSON.stringify(newValue), changed_by, reason ?? null);
10
+ }
11
+ // ─── 治理参数 ────────────────────────────────────────────────
12
+ app.get('/api/governance/params', (_req, res) => {
13
+ const params = db.prepare(`
14
+ SELECT key, value, type, description, category, default_value, min_value, max_value, updated_at
15
+ FROM protocol_params
16
+ ORDER BY category, key
17
+ `).all();
18
+ // 每个参数附最近 5 条变更
19
+ for (const p of params) {
20
+ const recent = db.prepare(`
21
+ SELECT old_value, new_value, action, created_at
22
+ FROM protocol_params_log
23
+ WHERE key = ?
24
+ ORDER BY id DESC LIMIT 5
25
+ `).all(p.key);
26
+ p.recent_changes = recent;
27
+ }
28
+ res.json({
29
+ notice: 'WebAZ 协议参数公示 — COP 团队自约束:所有参数变更必须可被任何人查询。',
30
+ params,
31
+ last_change: db.prepare(`SELECT key, old_value, new_value, action, created_at FROM protocol_params_log ORDER BY id DESC LIMIT 1`).get() || null,
32
+ });
33
+ });
34
+ app.get('/api/governance/params/:key/history', (req, res) => {
35
+ const param = db.prepare(`SELECT * FROM protocol_params WHERE key = ?`).get(req.params.key);
36
+ if (!param)
37
+ return void res.status(404).json({ error: 'param not found' });
38
+ const history = db.prepare(`
39
+ SELECT id, old_value, new_value, action, created_at,
40
+ (SELECT name FROM users WHERE id = protocol_params_log.changed_by) as changed_by_name
41
+ FROM protocol_params_log
42
+ WHERE key = ?
43
+ ORDER BY id DESC LIMIT 100
44
+ `).all(req.params.key);
45
+ res.json({ param, history });
46
+ });
47
+ // ─── 公共支付方法 ───────────────────────────────────────────
48
+ app.get('/api/payment-methods', (_req, res) => {
49
+ const rows = db.prepare(`SELECT id, display_name, display_name_en, kind, asset, chain, contract_address, decimals, icon, status, watcher_status, notes
50
+ FROM payment_methods WHERE status IN ('active','preview') ORDER BY status DESC, kind, asset`).all();
51
+ res.json({ items: rows });
52
+ });
53
+ // 某地区可用方法(fallback 到 global)
54
+ app.get('/api/payment-methods/for-region', (req, res) => {
55
+ const region = String(req.query.region || 'global');
56
+ const direction = String(req.query.direction || ''); // 'deposit' | 'withdraw' | '' (任意)
57
+ if (direction && !RPM_DIRECTIONS.has(direction)) {
58
+ return void res.status(400).json({ error: `direction 必须是 ${[...RPM_DIRECTIONS].join(' / ')}` });
59
+ }
60
+ const rowsRegion = db.prepare(`
61
+ SELECT rpm.region, rpm.method_id, rpm.direction, rpm.status, rpm.min_amount, rpm.max_amount, rpm.daily_cap, rpm.notes,
62
+ pm.display_name, pm.display_name_en, pm.kind, pm.asset, pm.chain, pm.icon, pm.status as method_status, pm.watcher_status
63
+ FROM region_payment_methods rpm JOIN payment_methods pm ON pm.id = rpm.method_id
64
+ WHERE rpm.region = ? AND rpm.status = 'active' AND pm.status IN ('active','preview')
65
+ `).all(region);
66
+ const useFallback = rowsRegion.length === 0 && region !== 'global';
67
+ const finalRegion = useFallback ? 'global' : region;
68
+ const rows = useFallback ? db.prepare(`
69
+ SELECT rpm.region, rpm.method_id, rpm.direction, rpm.status, rpm.min_amount, rpm.max_amount, rpm.daily_cap, rpm.notes,
70
+ pm.display_name, pm.display_name_en, pm.kind, pm.asset, pm.chain, pm.icon, pm.status as method_status, pm.watcher_status
71
+ FROM region_payment_methods rpm JOIN payment_methods pm ON pm.id = rpm.method_id
72
+ WHERE rpm.region = 'global' AND rpm.status = 'active' AND pm.status IN ('active','preview')
73
+ `).all() : rowsRegion;
74
+ const filtered = direction
75
+ ? rows.filter(r => r.direction === direction || r.direction === 'both')
76
+ : rows;
77
+ res.json({ region: finalRegion, fallback_from: useFallback ? region : null, items: filtered });
78
+ });
79
+ // 公共变更审计日志(COP transparency)
80
+ app.get('/api/payment-methods/log', (req, res) => {
81
+ const limit = Math.min(200, Math.max(10, Number(req.query.limit) || 50));
82
+ const rows = db.prepare(`SELECT id, entity_kind, entity_id, action, old_value, new_value, changed_by, reason, created_at
83
+ FROM payment_methods_log ORDER BY id DESC LIMIT ?`).all(limit);
84
+ res.json({ items: rows });
85
+ });
86
+ // ─── Admin payment_methods CRUD(root admin only · 基础设施变更需根权限)─
87
+ app.get('/api/admin/payment-methods', (req, res) => {
88
+ const user = requireRootAdmin(req, res);
89
+ if (!user)
90
+ return;
91
+ const rows = db.prepare(`SELECT * FROM payment_methods ORDER BY status DESC, kind, asset`).all();
92
+ res.json({ items: rows });
93
+ });
94
+ app.post('/api/admin/payment-methods', (req, res) => {
95
+ const user = requireRootAdmin(req, res);
96
+ if (!user)
97
+ return;
98
+ const b = req.body;
99
+ const id = String(b.id || '').trim().toLowerCase();
100
+ if (!/^[a-z0-9_]{3,40}$/.test(id))
101
+ return void res.status(400).json({ error: 'id 必须是 3-40 位 [a-z0-9_](如 usdc_base)' });
102
+ if (db.prepare(`SELECT 1 FROM payment_methods WHERE id = ?`).get(id))
103
+ return void res.status(409).json({ error: 'id 已存在' });
104
+ const display_name = String(b.display_name || '').trim();
105
+ if (display_name.length < 1 || display_name.length > 60)
106
+ return void res.status(400).json({ error: 'display_name 1-60 字' });
107
+ const display_name_en = b.display_name_en ? String(b.display_name_en).slice(0, 60) : null;
108
+ const kind = String(b.kind || '');
109
+ if (!PAYMENT_METHOD_KINDS.has(kind))
110
+ return void res.status(400).json({ error: `kind 必须是 ${[...PAYMENT_METHOD_KINDS].join(' / ')}` });
111
+ const asset = String(b.asset || '').trim().toUpperCase();
112
+ if (!/^[A-Z]{2,10}$/.test(asset))
113
+ return void res.status(400).json({ error: 'asset 必须是 2-10 位大写字母(如 USDC)' });
114
+ const chain = b.chain ? String(b.chain).trim().toLowerCase().slice(0, 20) : null;
115
+ const contract_address = b.contract_address ? String(b.contract_address).trim().slice(0, 80) : null;
116
+ const decimals = Number.isFinite(Number(b.decimals)) ? Number(b.decimals) : 6;
117
+ if (decimals < 0 || decimals > 18)
118
+ return void res.status(400).json({ error: 'decimals 0-18' });
119
+ const icon = b.icon ? String(b.icon).slice(0, 8) : null;
120
+ const status = String(b.status || 'inactive');
121
+ if (!PAYMENT_METHOD_STATUSES.has(status))
122
+ return void res.status(400).json({ error: `status 必须是 ${[...PAYMENT_METHOD_STATUSES].join(' / ')}` });
123
+ const notes = b.notes ? String(b.notes).slice(0, 200) : null;
124
+ const newRow = { id, display_name, display_name_en, kind, asset, chain, contract_address, decimals, icon, status, watcher_status: 'unconfigured', notes };
125
+ db.prepare(`INSERT INTO payment_methods (
126
+ id, display_name, display_name_en, kind, asset, chain, contract_address, decimals, icon, status, watcher_status, notes, updated_by
127
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, display_name, display_name_en, kind, asset, chain, contract_address, decimals, icon, status, 'unconfigured', notes, user.id);
128
+ logPaymentChange('method', id, 'create', null, newRow, user.id, String(b.reason || ''));
129
+ res.json({ ok: true, id });
130
+ });
131
+ app.put('/api/admin/payment-methods/:id', (req, res) => {
132
+ const user = requireRootAdmin(req, res);
133
+ if (!user)
134
+ return;
135
+ const id = req.params.id;
136
+ const existing = db.prepare(`SELECT * FROM payment_methods WHERE id = ?`).get(id);
137
+ if (!existing)
138
+ return void res.status(404).json({ error: 'not_found' });
139
+ const b = req.body;
140
+ const updates = {};
141
+ if (b.display_name !== undefined) {
142
+ const v = String(b.display_name).trim();
143
+ if (v.length < 1 || v.length > 60)
144
+ return void res.status(400).json({ error: 'display_name 1-60 字' });
145
+ updates.display_name = v;
146
+ }
147
+ if (b.display_name_en !== undefined)
148
+ updates.display_name_en = b.display_name_en ? String(b.display_name_en).slice(0, 60) : null;
149
+ if (b.status !== undefined) {
150
+ if (!PAYMENT_METHOD_STATUSES.has(String(b.status)))
151
+ return void res.status(400).json({ error: 'status 非法' });
152
+ updates.status = String(b.status);
153
+ }
154
+ if (b.chain !== undefined)
155
+ updates.chain = b.chain ? String(b.chain).trim().toLowerCase().slice(0, 20) : null;
156
+ if (b.contract_address !== undefined)
157
+ updates.contract_address = b.contract_address ? String(b.contract_address).trim().slice(0, 80) : null;
158
+ if (b.decimals !== undefined) {
159
+ const d = Number(b.decimals);
160
+ if (!Number.isFinite(d) || d < 0 || d > 18)
161
+ return void res.status(400).json({ error: 'decimals 0-18' });
162
+ updates.decimals = d;
163
+ }
164
+ if (b.icon !== undefined)
165
+ updates.icon = b.icon ? String(b.icon).slice(0, 8) : null;
166
+ if (b.notes !== undefined)
167
+ updates.notes = b.notes ? String(b.notes).slice(0, 200) : null;
168
+ if (b.watcher_status !== undefined) {
169
+ const ws = String(b.watcher_status);
170
+ if (!['active', 'unconfigured', 'failing'].includes(ws))
171
+ return void res.status(400).json({ error: 'watcher_status 非法' });
172
+ updates.watcher_status = ws;
173
+ }
174
+ if (Object.keys(updates).length === 0)
175
+ return void res.status(400).json({ error: '无更新字段' });
176
+ const cols = Object.keys(updates).map(k => `${k} = ?`).join(', ');
177
+ const vals = Object.values(updates);
178
+ db.prepare(`UPDATE payment_methods SET ${cols}, updated_at = datetime('now'), updated_by = ? WHERE id = ?`).run(...vals, user.id, id);
179
+ logPaymentChange('method', id, 'update', existing, { ...existing, ...updates }, user.id, String(b.reason || ''));
180
+ res.json({ ok: true });
181
+ });
182
+ app.delete('/api/admin/payment-methods/:id', (req, res) => {
183
+ const user = requireRootAdmin(req, res);
184
+ if (!user)
185
+ return;
186
+ const id = req.params.id;
187
+ if (id === 'usdc_base')
188
+ return void res.status(400).json({ error: '默认协议方法 usdc_base 不可删除,可改为 deprecated 状态' });
189
+ const existing = db.prepare(`SELECT * FROM payment_methods WHERE id = ?`).get(id);
190
+ if (!existing)
191
+ return void res.status(404).json({ error: 'not_found' });
192
+ const refs = db.prepare(`SELECT COUNT(*) as n FROM region_payment_methods WHERE method_id = ? AND status = 'active'`).get(id).n;
193
+ if (refs > 0)
194
+ return void res.status(409).json({ error: `还有 ${refs} 条 active 区域映射引用该方法,请先停用` });
195
+ db.prepare(`DELETE FROM payment_methods WHERE id = ?`).run(id);
196
+ logPaymentChange('method', id, 'delete', existing, null, user.id, String((req.body || {}).reason || ''));
197
+ res.json({ ok: true });
198
+ });
199
+ // ─── region_payment_methods CRUD ──────────────────────────
200
+ app.get('/api/admin/region-payment-methods', (req, res) => {
201
+ const user = requireRootAdmin(req, res);
202
+ if (!user)
203
+ return;
204
+ const where = [];
205
+ const params = [];
206
+ if (req.query.region) {
207
+ where.push('rpm.region = ?');
208
+ params.push(String(req.query.region));
209
+ }
210
+ if (req.query.method_id) {
211
+ where.push('rpm.method_id = ?');
212
+ params.push(String(req.query.method_id));
213
+ }
214
+ const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
215
+ const rows = db.prepare(`
216
+ SELECT rpm.*, pm.display_name, pm.display_name_en, pm.icon, pm.asset, pm.chain
217
+ FROM region_payment_methods rpm JOIN payment_methods pm ON pm.id = rpm.method_id
218
+ ${whereSql}
219
+ ORDER BY rpm.region, pm.kind, pm.asset
220
+ `).all(...params);
221
+ res.json({ items: rows });
222
+ });
223
+ app.post('/api/admin/region-payment-methods', (req, res) => {
224
+ const user = requireRootAdmin(req, res);
225
+ if (!user)
226
+ return;
227
+ const b = req.body;
228
+ const region = String(b.region || '').trim().toLowerCase();
229
+ if (!/^[a-z_]{2,20}$/.test(region))
230
+ return void res.status(400).json({ error: 'region 必须是 2-20 位 [a-z_]' });
231
+ const method_id = String(b.method_id || '');
232
+ if (!db.prepare(`SELECT 1 FROM payment_methods WHERE id = ?`).get(method_id))
233
+ return void res.status(404).json({ error: 'method_id 不存在' });
234
+ const direction = String(b.direction || 'both');
235
+ if (!RPM_DIRECTIONS.has(direction))
236
+ return void res.status(400).json({ error: `direction 必须是 ${[...RPM_DIRECTIONS].join(' / ')}` });
237
+ const status = String(b.status || 'active');
238
+ if (!RPM_STATUSES.has(status))
239
+ return void res.status(400).json({ error: `status 必须是 ${[...RPM_STATUSES].join(' / ')}` });
240
+ const min_amount = b.min_amount != null ? Math.max(0, Number(b.min_amount)) : 0;
241
+ const max_amount = b.max_amount != null && b.max_amount !== '' ? Math.max(0, Number(b.max_amount)) : null;
242
+ const daily_cap = b.daily_cap != null && b.daily_cap !== '' ? Math.max(0, Number(b.daily_cap)) : null;
243
+ if (max_amount != null && min_amount > max_amount)
244
+ return void res.status(400).json({ error: 'min_amount 不能大于 max_amount' });
245
+ const notes = b.notes ? String(b.notes).slice(0, 200) : null;
246
+ const id = generateId('rpm');
247
+ try {
248
+ db.prepare(`INSERT INTO region_payment_methods (
249
+ id, region, method_id, direction, status, min_amount, max_amount, daily_cap, notes, updated_by
250
+ ) VALUES (?,?,?,?,?,?,?,?,?,?)`).run(id, region, method_id, direction, status, min_amount, max_amount, daily_cap, notes, user.id);
251
+ }
252
+ catch (e) {
253
+ if (String(e).includes('UNIQUE'))
254
+ return void res.status(409).json({ error: '同一 region + method + direction 已存在' });
255
+ throw e;
256
+ }
257
+ logPaymentChange('region_mapping', id, 'create', null, { region, method_id, direction, status, min_amount, max_amount, daily_cap, notes }, user.id, String(b.reason || ''));
258
+ res.json({ ok: true, id });
259
+ });
260
+ app.put('/api/admin/region-payment-methods/:id', (req, res) => {
261
+ const user = requireRootAdmin(req, res);
262
+ if (!user)
263
+ return;
264
+ const id = req.params.id;
265
+ const existing = db.prepare(`SELECT * FROM region_payment_methods WHERE id = ?`).get(id);
266
+ if (!existing)
267
+ return void res.status(404).json({ error: 'not_found' });
268
+ const b = req.body;
269
+ const updates = {};
270
+ if (b.status !== undefined) {
271
+ if (!RPM_STATUSES.has(String(b.status)))
272
+ return void res.status(400).json({ error: 'status 非法' });
273
+ updates.status = String(b.status);
274
+ }
275
+ if (b.min_amount !== undefined)
276
+ updates.min_amount = Math.max(0, Number(b.min_amount));
277
+ if (b.max_amount !== undefined)
278
+ updates.max_amount = b.max_amount === null || b.max_amount === '' ? null : Math.max(0, Number(b.max_amount));
279
+ if (b.daily_cap !== undefined)
280
+ updates.daily_cap = b.daily_cap === null || b.daily_cap === '' ? null : Math.max(0, Number(b.daily_cap));
281
+ if (b.notes !== undefined)
282
+ updates.notes = b.notes ? String(b.notes).slice(0, 200) : null;
283
+ if (Object.keys(updates).length === 0)
284
+ return void res.status(400).json({ error: '无更新字段' });
285
+ // min/max 交叉校验
286
+ const newMin = updates.min_amount != null ? Number(updates.min_amount) : Number(existing.min_amount);
287
+ const newMax = updates.max_amount !== undefined ? updates.max_amount : existing.max_amount;
288
+ if (newMax != null && newMin > newMax)
289
+ return void res.status(400).json({ error: 'min_amount 不能大于 max_amount' });
290
+ const cols = Object.keys(updates).map(k => `${k} = ?`).join(', ');
291
+ const vals = Object.values(updates);
292
+ db.prepare(`UPDATE region_payment_methods SET ${cols}, updated_at = datetime('now'), updated_by = ? WHERE id = ?`).run(...vals, user.id, id);
293
+ logPaymentChange('region_mapping', id, 'update', existing, { ...existing, ...updates }, user.id, String(b.reason || ''));
294
+ res.json({ ok: true });
295
+ });
296
+ app.delete('/api/admin/region-payment-methods/:id', (req, res) => {
297
+ const user = requireRootAdmin(req, res);
298
+ if (!user)
299
+ return;
300
+ const id = req.params.id;
301
+ const existing = db.prepare(`SELECT * FROM region_payment_methods WHERE id = ?`).get(id);
302
+ if (!existing)
303
+ return void res.status(404).json({ error: 'not_found' });
304
+ if (existing.region === 'global' && existing.method_id === 'usdc_base') {
305
+ return void res.status(400).json({ error: '默认协议映射 global × usdc_base 不可删除' });
306
+ }
307
+ db.prepare(`DELETE FROM region_payment_methods WHERE id = ?`).run(id);
308
+ logPaymentChange('region_mapping', id, 'delete', existing, null, user.id, String((req.body || {}).reason || ''));
309
+ res.json({ ok: true });
310
+ });
311
+ }
@@ -0,0 +1,34 @@
1
+ export function registerPeersRoutes(app, deps) {
2
+ const { db, auth } = deps;
3
+ app.post('/api/peers/heartbeat', (req, res) => {
4
+ const me = auth(req, res);
5
+ if (!me)
6
+ return;
7
+ const hashes = Array.isArray(req.body?.hashes) ? req.body.hashes : [];
8
+ const pinIntents = Array.isArray(req.body?.pin_intents) ? new Set(req.body.pin_intents) : new Set();
9
+ const now = new Date().toISOString();
10
+ let registered = 0;
11
+ for (const h of hashes) {
12
+ if (typeof h !== 'string' || !/^[a-f0-9]{64}$/.test(h))
13
+ continue;
14
+ const m = db.prepare("SELECT owner_id, status FROM manifest_registry WHERE hash = ?").get(h);
15
+ if (!m || m.status !== 'active')
16
+ continue;
17
+ const isOwner = m.owner_id === me.id ? 1 : 0;
18
+ const pinIntent = pinIntents.has(h) ? 1 : 0;
19
+ db.prepare(`INSERT INTO peer_directory (peer_id, manifest_hash, is_owner, pin_intent, last_heartbeat)
20
+ VALUES (?,?,?,?,?)
21
+ ON CONFLICT(peer_id, manifest_hash) DO UPDATE SET is_owner=excluded.is_owner, pin_intent=excluded.pin_intent, last_heartbeat=excluded.last_heartbeat`)
22
+ .run(me.id, h, isOwner, pinIntent, now);
23
+ registered++;
24
+ }
25
+ res.json({ ok: true, registered });
26
+ });
27
+ app.delete('/api/peers/:hash', (req, res) => {
28
+ const me = auth(req, res);
29
+ if (!me)
30
+ return;
31
+ db.prepare("DELETE FROM peer_directory WHERE peer_id = ? AND manifest_hash = ?").run(me.id, req.params.hash);
32
+ res.json({ ok: true });
33
+ });
34
+ }
@@ -0,0 +1,39 @@
1
+ export function registerPinReceiptsRoutes(app, deps) {
2
+ const { db, auth, generateId } = deps;
3
+ app.post('/api/pin-receipts', (req, res) => {
4
+ const me = auth(req, res);
5
+ if (!me)
6
+ return;
7
+ const { manifest_hash, pinner_id, bytes_served, served_at, pinner_sig, recipient_sig } = req.body || {};
8
+ if (!manifest_hash || !pinner_id || !pinner_sig || !recipient_sig || !served_at) {
9
+ return void res.json({ error: '缺少字段' });
10
+ }
11
+ if (typeof bytes_served !== 'number' || bytes_served <= 0 || bytes_served > 500 * 1024 * 1024) {
12
+ return void res.json({ error: 'bytes_served 不合法' });
13
+ }
14
+ if (pinner_id === me.id)
15
+ return void res.json({ error: 'pinner 不能是自己' });
16
+ const m = db.prepare("SELECT 1 FROM manifest_registry WHERE hash = ? AND status = 'active'").get(manifest_hash);
17
+ if (!m)
18
+ return void res.json({ error: 'manifest 不存在或已下架' });
19
+ const dup = db.prepare(`SELECT id FROM pin_receipts WHERE manifest_hash = ? AND pinner_id = ? AND recipient_id = ? AND served_at > datetime('now', '-1 day')`).get(manifest_hash, pinner_id, me.id);
20
+ if (dup)
21
+ return void res.json({ error: '24 小时内已有相同 pin 回执' });
22
+ const id = generateId('pin');
23
+ db.prepare(`INSERT INTO pin_receipts (id, manifest_hash, pinner_id, recipient_id, bytes_served, served_at, pinner_sig, recipient_sig)
24
+ VALUES (?,?,?,?,?,?,?,?)`)
25
+ .run(id, manifest_hash, pinner_id, me.id, bytes_served, served_at, pinner_sig, recipient_sig);
26
+ db.prepare(`UPDATE peer_directory SET bytes_served_total = bytes_served_total + ? WHERE peer_id = ? AND manifest_hash = ?`)
27
+ .run(bytes_served, pinner_id, manifest_hash);
28
+ res.json({ ok: true, id });
29
+ });
30
+ app.get('/api/pin-receipts/mine', (req, res) => {
31
+ const me = auth(req, res);
32
+ if (!me)
33
+ return;
34
+ const earned = db.prepare(`SELECT COALESCE(SUM(rewarded_waz), 0) as total, COUNT(*) as count FROM pin_receipts WHERE pinner_id = ? AND rewarded_at IS NOT NULL`).get(me.id);
35
+ const pending = db.prepare(`SELECT COUNT(*) as n FROM pin_receipts WHERE pinner_id = ? AND rewarded_at IS NULL`).get(me.id);
36
+ const recent = db.prepare(`SELECT p.*, m.title as manifest_title FROM pin_receipts p LEFT JOIN manifest_registry m ON m.hash = p.manifest_hash WHERE p.pinner_id = ? ORDER BY p.served_at DESC LIMIT 20`).all(me.id);
37
+ res.json({ earned, pending_count: pending.n, recent });
38
+ });
39
+ }