@seasonkoh/webaz 0.1.8 → 0.1.10

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 (154) 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 +3679 -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 +31362 -2320
  22. package/dist/pwa/public/i18n.js +5308 -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 +386 -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/ap2-mandate.js +121 -0
  58. package/dist/pwa/routes/arbitrator.js +117 -0
  59. package/dist/pwa/routes/auction.js +436 -0
  60. package/dist/pwa/routes/auth-login.js +40 -0
  61. package/dist/pwa/routes/auth-read.js +66 -0
  62. package/dist/pwa/routes/auth-register.js +161 -0
  63. package/dist/pwa/routes/auth-sessions.js +62 -0
  64. package/dist/pwa/routes/blocklist.js +60 -0
  65. package/dist/pwa/routes/buyer-feeds.js +224 -0
  66. package/dist/pwa/routes/cart.js +155 -0
  67. package/dist/pwa/routes/charity.js +816 -0
  68. package/dist/pwa/routes/chat.js +318 -0
  69. package/dist/pwa/routes/checkin-tasks.js +122 -0
  70. package/dist/pwa/routes/checkout-helpers.js +106 -0
  71. package/dist/pwa/routes/claim-initiators.js +88 -0
  72. package/dist/pwa/routes/claim-verify.js +615 -0
  73. package/dist/pwa/routes/claim-voting.js +114 -0
  74. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  75. package/dist/pwa/routes/coupons.js +165 -0
  76. package/dist/pwa/routes/dashboards.js +99 -0
  77. package/dist/pwa/routes/dispute-cases.js +267 -0
  78. package/dist/pwa/routes/disputes-read.js +358 -0
  79. package/dist/pwa/routes/disputes-write.js +475 -0
  80. package/dist/pwa/routes/evidence.js +86 -0
  81. package/dist/pwa/routes/external-anchors.js +107 -0
  82. package/dist/pwa/routes/feedback.js +270 -0
  83. package/dist/pwa/routes/flash-sales.js +130 -0
  84. package/dist/pwa/routes/follows.js +103 -0
  85. package/dist/pwa/routes/group-buys.js +208 -0
  86. package/dist/pwa/routes/growth.js +199 -0
  87. package/dist/pwa/routes/import-product.js +153 -0
  88. package/dist/pwa/routes/kyc.js +40 -0
  89. package/dist/pwa/routes/leaderboard.js +149 -0
  90. package/dist/pwa/routes/listings.js +281 -0
  91. package/dist/pwa/routes/logistics.js +35 -0
  92. package/dist/pwa/routes/manifests.js +126 -0
  93. package/dist/pwa/routes/me-data.js +101 -0
  94. package/dist/pwa/routes/notifications.js +48 -0
  95. package/dist/pwa/routes/offers.js +96 -0
  96. package/dist/pwa/routes/orders-action.js +285 -0
  97. package/dist/pwa/routes/orders-create.js +382 -0
  98. package/dist/pwa/routes/orders-read.js +180 -0
  99. package/dist/pwa/routes/p2p-products.js +178 -0
  100. package/dist/pwa/routes/payments-governance.js +311 -0
  101. package/dist/pwa/routes/peers.js +34 -0
  102. package/dist/pwa/routes/pin-receipts.js +39 -0
  103. package/dist/pwa/routes/products-aliases.js +119 -0
  104. package/dist/pwa/routes/products-claims.js +60 -0
  105. package/dist/pwa/routes/products-create.js +206 -0
  106. package/dist/pwa/routes/products-crud.js +73 -0
  107. package/dist/pwa/routes/products-links.js +129 -0
  108. package/dist/pwa/routes/products-list.js +424 -0
  109. package/dist/pwa/routes/products-meta.js +155 -0
  110. package/dist/pwa/routes/products-update.js +125 -0
  111. package/dist/pwa/routes/profile-credentials.js +105 -0
  112. package/dist/pwa/routes/profile-identity.js +174 -0
  113. package/dist/pwa/routes/profile-location.js +35 -0
  114. package/dist/pwa/routes/profile-placement.js +70 -0
  115. package/dist/pwa/routes/profile-prefs.js +93 -0
  116. package/dist/pwa/routes/promoter.js +208 -0
  117. package/dist/pwa/routes/public-utils.js +227 -0
  118. package/dist/pwa/routes/push.js +54 -0
  119. package/dist/pwa/routes/ratings.js +220 -0
  120. package/dist/pwa/routes/recover-key.js +100 -0
  121. package/dist/pwa/routes/referral.js +58 -0
  122. package/dist/pwa/routes/reputation.js +34 -0
  123. package/dist/pwa/routes/returns.js +493 -0
  124. package/dist/pwa/routes/reviews.js +81 -0
  125. package/dist/pwa/routes/rfqs.js +443 -0
  126. package/dist/pwa/routes/search.js +172 -0
  127. package/dist/pwa/routes/secondhand.js +278 -0
  128. package/dist/pwa/routes/seller-quota.js +225 -0
  129. package/dist/pwa/routes/share-redirects.js +164 -0
  130. package/dist/pwa/routes/shareables-interactions.js +212 -0
  131. package/dist/pwa/routes/shareables.js +470 -0
  132. package/dist/pwa/routes/shops.js +98 -0
  133. package/dist/pwa/routes/signaling.js +43 -0
  134. package/dist/pwa/routes/skill-market.js +173 -0
  135. package/dist/pwa/routes/skills.js +174 -0
  136. package/dist/pwa/routes/snf.js +126 -0
  137. package/dist/pwa/routes/tags.js +47 -0
  138. package/dist/pwa/routes/trial.js +333 -0
  139. package/dist/pwa/routes/trusted-kpi.js +87 -0
  140. package/dist/pwa/routes/url-claim.js +113 -0
  141. package/dist/pwa/routes/users-public.js +317 -0
  142. package/dist/pwa/routes/variants.js +156 -0
  143. package/dist/pwa/routes/verifier-user.js +107 -0
  144. package/dist/pwa/routes/verify-tasks.js +120 -0
  145. package/dist/pwa/routes/waitlist.js +65 -0
  146. package/dist/pwa/routes/wallet-read.js +218 -0
  147. package/dist/pwa/routes/wallet-write.js +273 -0
  148. package/dist/pwa/routes/webauthn.js +188 -0
  149. package/dist/pwa/routes/webhooks.js +162 -0
  150. package/dist/pwa/routes/welcome.js +226 -0
  151. package/dist/pwa/routes/wishlist-qa.js +135 -0
  152. package/dist/pwa/security/ssrf.js +110 -0
  153. package/dist/pwa/server.js +9317 -2097
  154. package/package.json +8 -3
@@ -0,0 +1,386 @@
1
+ import { computeAgentPassport } from '../../layer1-agent/L1-2-identity/agent-passport.js';
2
+ export function registerAgentGovernanceRoutes(app, deps) {
3
+ const { db, generateId, auth, requireRootAdmin, invalidateAgentBlockedCache, requireHumanPresence, issueAgentStrike, custodianFingerprint, signPassport, issuerAddress } = deps;
4
+ // /api/me/agents — 列出本账号所有 agent + declaration / strikes
5
+ app.get('/api/me/agents', (req, res) => {
6
+ const user = auth(req, res);
7
+ if (!user)
8
+ return;
9
+ const keys = db.prepare(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`).all(user.id, user.id);
10
+ const items = keys.map(k => {
11
+ const decl = db.prepare(`SELECT operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, revoked_at FROM agent_declarations WHERE api_key = ?`).get(k.api_key);
12
+ // P1 fix 4.4:附 signals JSON
13
+ const rep = db.prepare(`SELECT trust_score, level, signals, last_calculated_at FROM agent_reputation WHERE api_key = ?`).get(k.api_key);
14
+ if (rep && rep.signals) {
15
+ try {
16
+ rep.signals = JSON.parse(rep.signals);
17
+ }
18
+ catch {
19
+ rep.signals = null;
20
+ }
21
+ }
22
+ const calls30d = db.prepare(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`).get(k.api_key).n;
23
+ const last = db.prepare(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`).get(k.api_key);
24
+ const strikes = db.prepare(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`).all(k.api_key);
25
+ let passport = null;
26
+ try {
27
+ passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
28
+ }
29
+ catch { /* read-only, never break the list */ }
30
+ return {
31
+ api_key_prefix: k.api_key.slice(0, 12) + '...',
32
+ api_key_full: k.api_key === user.api_key ? k.api_key : null,
33
+ declaration: decl || null,
34
+ reputation: rep || null,
35
+ calls_30d: calls30d,
36
+ last_call: last || null,
37
+ recent_strikes: strikes,
38
+ passport,
39
+ };
40
+ });
41
+ // Phase 2 监护人总览(只读/软绑定):真人态 + 旗下 agent 聚合 + 连带
42
+ const hasPasskey = (db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?').get(user.id)?.n || 0) > 0;
43
+ const pps = items.map(i => i.passport).filter(Boolean);
44
+ const depthRank = { shallow: 0, medium: 1, deep: 2, profound: 3 };
45
+ const deepest = pps.reduce((d, p) => (depthRank[p.engagement_depth] ?? 0) > (depthRank[d] ?? 0) ? p.engagement_depth : d, 'shallow');
46
+ const custodian = {
47
+ fingerprint: custodianFingerprint(user.id),
48
+ has_passkey: hasPasskey,
49
+ agent_count: items.length,
50
+ max_risk: pps.reduce((m, p) => Math.max(m, p.risk_score), 0),
51
+ high_risk_count: pps.filter(p => p.risk_score >= 50).length,
52
+ deepest_engagement: pps.length ? deepest : null,
53
+ };
54
+ res.json({ items, custodian });
55
+ });
56
+ // Phase 4 + DID/VC 短期 mapping(B.6 b,2026-05-30):
57
+ // webaz_format = 原 WebAZAgentPassport (向后兼容,任何 existing consumer 还用这个)
58
+ // vc_format = W3C Verifiable Credential v1 标准(任何标准 DID/VC resolver 可用)
59
+ // 两者签名是同一个(eip191 over canonical 串),两者可互推。
60
+ // issuer 同时给 did:web:webaz.xyz(标准 DID method)+ 原 did:webaz:0x... 地址(向后兼容)。
61
+ app.get('/api/me/agents/:apiKeyPrefix/passport', async (req, res) => {
62
+ const user = auth(req, res);
63
+ if (!user)
64
+ return;
65
+ const prefix = String(req.params.apiKeyPrefix || '').replace('...', '');
66
+ if (prefix.length < 6)
67
+ return void res.status(400).json({ error: 'apiKeyPrefix 太短' });
68
+ const keys = db.prepare(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`).all(user.id, user.id);
69
+ const match = keys.find(k => k.api_key.startsWith(prefix));
70
+ if (!match)
71
+ return void res.status(404).json({ error: '未找到该 agent(或不属于你)' });
72
+ const pp = computeAgentPassport(db, match.api_key, user.id, custodianFingerprint);
73
+ const issued_at = new Date().toISOString();
74
+ const expires_at = new Date(Date.now() + 7 * 86400_000).toISOString();
75
+ const issuerAddr = issuerAddress();
76
+ const issuerDidWeb = 'did:web:webaz.xyz'; // W3C did:web 形态
77
+ const issuerDidLegacy = 'did:webaz:' + issuerAddr; // 原自定义形态(保留)
78
+ const keyPrefix = match.api_key.slice(0, 12) + '...';
79
+ const bp = pp.behavior_profile;
80
+ // 规范化签名串(verifier 用相同格式重建 → verifyMessage(issuerAddr, canonical, signature))
81
+ // 两套 wrapper 共享同一 canonical + signature,所以一签两用。
82
+ const canonical = `webaz-agent-passport|v1|${issuerAddr}|${issued_at}|${expires_at}|${pp.custodian_fingerprint}|${keyPrefix}|risk=${pp.risk_score}|depth=${pp.engagement_depth}|bp=${bp.query},${bp.transact},${bp.govern}`;
83
+ let signature = '';
84
+ try {
85
+ signature = await signPassport(canonical);
86
+ }
87
+ catch (e) {
88
+ return void res.status(503).json({ error: '签名服务暂不可用', detail: e.message });
89
+ }
90
+ // 原格式(向后兼容)— 任何已写过 webaz 集成的 consumer 不需要改
91
+ const webaz_format = {
92
+ type: 'WebAZAgentPassport', version: 1,
93
+ issuer: issuerDidLegacy, issuer_address: issuerAddr,
94
+ issued_at, expires_at,
95
+ subject: { custodian_fingerprint: pp.custodian_fingerprint, agent_key_prefix: keyPrefix },
96
+ claims: { risk_score: pp.risk_score, engagement_depth: pp.engagement_depth, behavior_profile: bp },
97
+ canonical, signature,
98
+ verify: { scheme: 'eip191', how: 'verifyMessage(issuer_address, canonical, signature)' },
99
+ };
100
+ // W3C Verifiable Credential v1 标准格式 — 任何 DID/VC 生态工具(KILT/Polygon ID/Veramo/SpruceID/...)可直接消费
101
+ // proof.type 用 EcdsaSecp256k1RecoverySignature2020(eip191 在 W3C 安全套件里的标准名)
102
+ // proofValue 是同一 signature 字符串(0x..),verifier 用 viem.verifyMessage 验真
103
+ const vc_format = {
104
+ '@context': [
105
+ 'https://www.w3.org/2018/credentials/v1',
106
+ 'https://webaz.xyz/credentials/v1', // webaz 自定义 schema 命名空间
107
+ ],
108
+ type: ['VerifiableCredential', 'WebAZAgentPassport'],
109
+ issuer: issuerDidWeb, // did:web — 通过 /.well-known/did.json 解析
110
+ issuanceDate: issued_at,
111
+ expirationDate: expires_at,
112
+ credentialSubject: {
113
+ id: `${issuerDidWeb}:agents:${keyPrefix.replace('...', '')}`, // 在 issuer 命名空间下的 agent 标识
114
+ custodianFingerprint: pp.custodian_fingerprint,
115
+ agentKeyPrefix: keyPrefix,
116
+ riskScore: pp.risk_score,
117
+ engagementDepth: pp.engagement_depth,
118
+ behaviorProfile: { query: bp.query, transact: bp.transact, govern: bp.govern },
119
+ },
120
+ proof: {
121
+ type: 'EcdsaSecp256k1RecoverySignature2020', // eip191 在 W3C 套件里的标准名
122
+ created: issued_at,
123
+ verificationMethod: `${issuerDidWeb}#key-1`, // 指向 did.json 里的 verificationMethod
124
+ proofPurpose: 'assertionMethod',
125
+ proofValue: signature,
126
+ // canonical 不在 W3C VC 标准里,但 webaz consumer 仍需要它来重建签名:
127
+ webazCanonical: canonical,
128
+ },
129
+ };
130
+ res.json({
131
+ // 两个格式并存返回 — backward-compat consumer 用 webaz_format,标准 DID/VC consumer 用 vc_format
132
+ // 顶层保留 webaz_format 的关键字段方便不解嵌的 consumer(类似 ?format=webaz 默认行为)
133
+ ...webaz_format,
134
+ vc_format,
135
+ webaz_format, // 显式重复让消费者明确选哪个
136
+ });
137
+ });
138
+ app.get('/api/me/agents/:apiKeyPrefix/log', (req, res) => {
139
+ const user = auth(req, res);
140
+ if (!user)
141
+ return;
142
+ const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
143
+ if (prefix.length < 8)
144
+ return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
145
+ const targetKey = db.prepare(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
146
+ UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`)
147
+ .get(user.id, prefix, user.id, prefix);
148
+ if (!targetKey)
149
+ return void res.status(404).json({ error: '未找到匹配的 agent api_key(仅可查本人的)' });
150
+ const limit = Math.min(500, Math.max(10, Number(req.query.limit) || 100));
151
+ const rows = db.prepare(`SELECT endpoint, method, status_code, created_at FROM agent_call_log
152
+ WHERE api_key = ? AND created_at > datetime('now', '-30 days') ORDER BY id DESC LIMIT ?`).all(targetKey.api_key, limit);
153
+ res.json({ items: rows });
154
+ });
155
+ app.post('/api/me/agents/declarations', (req, res) => {
156
+ const user = auth(req, res);
157
+ if (!user)
158
+ return;
159
+ const b = req.body;
160
+ const targetApiKey = b.api_key ? String(b.api_key) : user.api_key;
161
+ const ownership = db.prepare(`SELECT id FROM users WHERE id = ? AND api_key = ?
162
+ UNION SELECT user_id as id FROM agent_reputation WHERE user_id = ? AND api_key = ?`)
163
+ .get(user.id, targetApiKey, user.id, targetApiKey);
164
+ if (!ownership)
165
+ return void res.status(403).json({ error: 'api_key 不属于本账号' });
166
+ const operator_name = String(b.operator_name || '').trim().slice(0, 60);
167
+ if (operator_name.length < 2)
168
+ return void res.status(400).json({ error: 'operator_name 2-60 字' });
169
+ const operator_contact = String(b.operator_contact || '').trim().slice(0, 120);
170
+ if (operator_contact.length < 3)
171
+ return void res.status(400).json({ error: 'operator_contact 必填' });
172
+ const purpose = String(b.purpose || '').trim().slice(0, 200);
173
+ if (purpose.length < 5)
174
+ return void res.status(400).json({ error: 'purpose 5-200 字' });
175
+ let scopeJson;
176
+ try {
177
+ const s = b.declared_scope;
178
+ if (!s || typeof s !== 'object')
179
+ return void res.status(400).json({ error: 'declared_scope 必须是对象' });
180
+ scopeJson = JSON.stringify(s).slice(0, 2000);
181
+ }
182
+ catch {
183
+ return void res.status(400).json({ error: 'declared_scope 无法序列化' });
184
+ }
185
+ const attestationsJson = b.attestations && typeof b.attestations === 'object' ? JSON.stringify(b.attestations).slice(0, 2000) : null;
186
+ const repo_url = b.repo_url ? String(b.repo_url).slice(0, 200) : null;
187
+ const homepage = b.homepage ? String(b.homepage).slice(0, 200) : null;
188
+ db.prepare(`INSERT INTO agent_declarations (
189
+ api_key, user_id, operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, homepage
190
+ ) VALUES (?,?,?,?,?,?,?,?,?)
191
+ ON CONFLICT(api_key) DO UPDATE SET
192
+ operator_name = excluded.operator_name,
193
+ operator_contact = excluded.operator_contact,
194
+ purpose = excluded.purpose,
195
+ declared_scope = excluded.declared_scope,
196
+ attestations = excluded.attestations,
197
+ repo_url = excluded.repo_url,
198
+ homepage = excluded.homepage,
199
+ revoked_at = NULL,
200
+ updated_at = datetime('now')`).run(targetApiKey, user.id, operator_name, operator_contact, purpose, scopeJson, attestationsJson, repo_url, homepage);
201
+ invalidateAgentBlockedCache(targetApiKey);
202
+ res.json({ ok: true });
203
+ });
204
+ // 用户撤销 agent(铁律 §4 human presence)
205
+ app.post('/api/me/agents/:apiKeyPrefix/revoke', (req, res) => {
206
+ const user = auth(req, res);
207
+ if (!user)
208
+ return;
209
+ const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
210
+ if (prefix.length < 8)
211
+ return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
212
+ const targetKey = db.prepare(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
213
+ UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`)
214
+ .get(user.id, prefix, user.id, prefix);
215
+ if (!targetKey)
216
+ return void res.status(404).json({ error: '未找到匹配的 agent api_key' });
217
+ const hpCheck = requireHumanPresence(user.id, 'agent_revoke', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_revoke', () => true);
218
+ if (!hpCheck.ok)
219
+ return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
220
+ const reason = String((req.body || {}).reason || '').slice(0, 300);
221
+ db.prepare(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
222
+ VALUES ('api_key', ?, ?, 'self', ?)
223
+ ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`).run(targetKey.api_key, user.id, reason);
224
+ db.prepare(`UPDATE agent_declarations SET revoked_at = datetime('now'), revoked_reason = ? WHERE api_key = ?`).run(reason, targetKey.api_key);
225
+ invalidateAgentBlockedCache(targetKey.api_key);
226
+ res.json({ ok: true });
227
+ });
228
+ // 撤销同 operator 名下所有 agent(仅撤销本用户给 operator 旗下 agent 的 attestation)
229
+ app.post('/api/me/agents/operators/:operator_name/revoke', (req, res) => {
230
+ const user = auth(req, res);
231
+ if (!user)
232
+ return;
233
+ const opName = String(req.params.operator_name || '').trim().slice(0, 60);
234
+ if (opName.length < 2)
235
+ return void res.status(400).json({ error: 'operator_name 至少 2 字' });
236
+ const hpCheck = requireHumanPresence(user.id, 'agent_revoke', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_revoke', () => true);
237
+ if (!hpCheck.ok)
238
+ return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
239
+ const reason = String((req.body || {}).reason || '').slice(0, 300);
240
+ const affected = db.prepare(`UPDATE agent_attestations SET revoked_at = datetime('now')
241
+ WHERE user_id = ? AND revoked_at IS NULL
242
+ AND api_key IN (SELECT api_key FROM agent_declarations WHERE operator_name = ?)`)
243
+ .run(user.id, opName);
244
+ db.prepare(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
245
+ VALUES ('operator_name', ?, ?, 'self', ?)
246
+ ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`).run(opName, user.id, reason);
247
+ const keys = db.prepare(`SELECT api_key FROM agent_declarations WHERE operator_name = ?`).all(opName);
248
+ for (const k of keys)
249
+ invalidateAgentBlockedCache(k.api_key);
250
+ res.json({ ok: true, attestations_revoked: affected.changes });
251
+ });
252
+ // P0 audit fix 4.2: 申诉 strike
253
+ app.post('/api/me/agents/strikes/:strikeId/appeal', (req, res) => {
254
+ const user = auth(req, res);
255
+ if (!user)
256
+ return;
257
+ const strikeId = Number(req.params.strikeId);
258
+ if (!Number.isInteger(strikeId) || strikeId <= 0)
259
+ return void res.status(400).json({ error: 'strikeId 必须是正整数' });
260
+ const strike = db.prepare(`SELECT id, api_key, user_id, severity, issued_at, appeal_status FROM agent_strikes WHERE id = ?`).get(strikeId);
261
+ if (!strike)
262
+ return void res.status(404).json({ error: 'strike 不存在' });
263
+ if (strike.user_id !== user.id)
264
+ return void res.status(403).json({ error: '只能申诉自己 agent 的 strike' });
265
+ if (strike.appeal_status !== 'none')
266
+ return void res.status(409).json({ error: `已申诉过(状态:${strike.appeal_status})` });
267
+ const issuedAt = new Date(String(strike.issued_at).replace(' ', 'T') + 'Z').getTime();
268
+ if (Date.now() - issuedAt > 30 * 86400_000)
269
+ return void res.status(410).json({ error: '已过 30 天申诉窗口' });
270
+ const reason = String((req.body || {}).reason || '').trim().slice(0, 500);
271
+ if (reason.length < 10)
272
+ return void res.status(400).json({ error: '申诉理由 ≥10 字' });
273
+ db.prepare(`UPDATE agent_strikes SET appeal_status = 'pending', appeal_reason = ? WHERE id = ?`).run(reason, strikeId);
274
+ res.json({ ok: true, message: '申诉已提交,等待 root admin 审核' });
275
+ });
276
+ // Admin: 审核 strike 申诉
277
+ app.post('/api/admin/agent-strikes/:strikeId/decide', (req, res) => {
278
+ const user = requireRootAdmin(req, res);
279
+ if (!user)
280
+ return;
281
+ const strikeId = Number(req.params.strikeId);
282
+ if (!Number.isInteger(strikeId) || strikeId <= 0)
283
+ return void res.status(400).json({ error: 'strikeId 必须是正整数' });
284
+ const decision = String((req.body || {}).decision || '');
285
+ if (!['approved', 'denied'].includes(decision))
286
+ return void res.status(400).json({ error: 'decision 必须是 approved / denied' });
287
+ const strike = db.prepare(`SELECT id, api_key, appeal_status FROM agent_strikes WHERE id = ?`).get(strikeId);
288
+ if (!strike)
289
+ return void res.status(404).json({ error: 'strike 不存在' });
290
+ if (strike.appeal_status !== 'pending')
291
+ return void res.status(409).json({ error: `当前状态 ${strike.appeal_status} 不可裁决` });
292
+ db.prepare(`UPDATE agent_strikes SET appeal_status = ?, appeal_decided_by = ?, appeal_decided_at = datetime('now') WHERE id = ?`)
293
+ .run(decision, user.id, strikeId);
294
+ invalidateAgentBlockedCache(strike.api_key);
295
+ // P1 fix 5.3: appeal approved → 恢复因 strike 自动停用的 skills
296
+ if (decision === 'approved') {
297
+ try {
298
+ const uRow = db.prepare(`SELECT id FROM users WHERE api_key = ?`).get(strike.api_key);
299
+ if (uRow) {
300
+ const r = db.prepare(`UPDATE skills SET active = 1, disabled_by_strike_at = NULL
301
+ WHERE seller_id = ? AND disabled_by_strike_at IS NOT NULL AND active = 0`).run(uRow.id);
302
+ if (r.changes > 0)
303
+ console.log(`[appeal approved→skill] restored ${r.changes} skills for ${uRow.id}`);
304
+ }
305
+ }
306
+ catch (e) {
307
+ console.error('[appeal skills restore]', e);
308
+ }
309
+ }
310
+ res.json({ ok: true, decision });
311
+ });
312
+ // Admin: 列出待审 strike 申诉
313
+ app.get('/api/admin/agent-strikes/pending', (req, res) => {
314
+ const user = requireRootAdmin(req, res);
315
+ if (!user)
316
+ return;
317
+ const rows = db.prepare(`SELECT s.id, s.api_key, s.user_id, u.handle, s.severity, s.reason_code, s.reason_detail, s.issued_at, s.appeal_reason
318
+ FROM agent_strikes s JOIN users u ON u.id = s.user_id
319
+ WHERE s.appeal_status = 'pending' ORDER BY s.id DESC LIMIT 100`).all();
320
+ res.json({ items: rows });
321
+ });
322
+ // P1 fix 4.3: admin 主动 issue strike
323
+ app.post('/api/admin/agent-strikes/issue', (req, res) => {
324
+ const adminUser = requireRootAdmin(req, res);
325
+ if (!adminUser)
326
+ return;
327
+ const b = req.body;
328
+ const apiKey = String(b.api_key || '').trim();
329
+ if (apiKey.length < 8)
330
+ return void res.status(400).json({ error: 'api_key 必填' });
331
+ const targetUser = db.prepare(`SELECT id, handle FROM users WHERE api_key = ?`).get(apiKey);
332
+ if (!targetUser)
333
+ return void res.status(404).json({ error: '未找到该 api_key' });
334
+ const reasonCode = String(b.reason_code || '').trim().slice(0, 40);
335
+ if (!/^[a-z_]{3,40}$/.test(reasonCode))
336
+ return void res.status(400).json({ error: 'reason_code 必须是 3-40 位 [a-z_](如 fake_shipment / spam)' });
337
+ const reasonDetail = b.reason_detail ? String(b.reason_detail).slice(0, 500) : null;
338
+ const initialSeverity = (b.severity ? String(b.severity) : 'warning');
339
+ if (!['warning', 'suspend_7d', 'permanent'].includes(initialSeverity)) {
340
+ return void res.status(400).json({ error: 'severity 必须是 warning / suspend_7d / permanent' });
341
+ }
342
+ const result = issueAgentStrike({
343
+ apiKey, userId: targetUser.id,
344
+ reasonCode, reasonDetail: reasonDetail || undefined,
345
+ reportedBy: adminUser.id,
346
+ relatedRef: b.related_ref ? String(b.related_ref) : undefined,
347
+ initialSeverity,
348
+ });
349
+ res.json({ ok: true, target_handle: targetUser.handle, ...result });
350
+ });
351
+ // bilateral attestation(用户批准某 agent 的 scope)
352
+ app.post('/api/me/agents/attestations', (req, res) => {
353
+ const user = auth(req, res);
354
+ if (!user)
355
+ return;
356
+ const b = req.body;
357
+ const apiKey = String(b.api_key || '');
358
+ if (!apiKey)
359
+ return void res.status(400).json({ error: 'api_key 必填' });
360
+ const decl = db.prepare(`SELECT declared_scope, operator_name, purpose FROM agent_declarations WHERE api_key = ? AND revoked_at IS NULL`).get(apiKey);
361
+ if (!decl)
362
+ return void res.status(404).json({ error: '该 agent 未声明 / 已撤销,无法授权' });
363
+ let approvedScopeJson;
364
+ try {
365
+ const s = b.approved_scope;
366
+ if (!s || typeof s !== 'object')
367
+ return void res.status(400).json({ error: 'approved_scope 必须是对象' });
368
+ approvedScopeJson = JSON.stringify(s).slice(0, 2000);
369
+ }
370
+ catch {
371
+ return void res.status(400).json({ error: 'approved_scope 无法序列化' });
372
+ }
373
+ const spendCapPerOrder = b.spend_cap_per_order != null ? Math.max(0, Number(b.spend_cap_per_order)) : null;
374
+ const spendCapDaily = b.spend_cap_daily != null ? Math.max(0, Number(b.spend_cap_daily)) : null;
375
+ const id = generateId('aat');
376
+ db.prepare(`INSERT INTO agent_attestations (id, api_key, user_id, approved_scope, spend_cap_per_order, spend_cap_daily)
377
+ VALUES (?,?,?,?,?,?)
378
+ ON CONFLICT(api_key, user_id) DO UPDATE SET
379
+ approved_scope = excluded.approved_scope,
380
+ spend_cap_per_order = excluded.spend_cap_per_order,
381
+ spend_cap_daily = excluded.spend_cap_daily,
382
+ revoked_at = NULL,
383
+ granted_at = datetime('now')`).run(id, apiKey, user.id, approvedScopeJson, spendCapPerOrder, spendCapDaily);
384
+ res.json({ ok: true });
385
+ });
386
+ }
@@ -0,0 +1,34 @@
1
+ export function registerAgentReputationRoutes(app, deps) {
2
+ const { auth, getAgentTrustCached, getRawModeMinTrust } = deps;
3
+ app.get('/api/agents/me/reputation', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ const key = req.headers.authorization?.replace('Bearer ', '') ?? '';
8
+ const t = getAgentTrustCached(key);
9
+ if (!t)
10
+ return void res.status(404).json({ error: 'agent_not_found' });
11
+ const min = getRawModeMinTrust();
12
+ res.json({
13
+ api_key_prefix: key.slice(0, 12) + '…',
14
+ user_id: t.user_id,
15
+ trust_score: t.trust_score,
16
+ level: t.level,
17
+ signals: t.signals,
18
+ raw_mode_enabled: t.trust_score >= min,
19
+ raw_mode_min_trust: min,
20
+ });
21
+ });
22
+ app.get('/api/admin/agents/:api_key/reputation', (req, res) => {
23
+ const admin = auth(req, res);
24
+ if (!admin)
25
+ return;
26
+ if (admin.role !== 'admin')
27
+ return void res.status(403).json({ error: '仅管理员可查询其他 agent' });
28
+ const target = req.params.api_key;
29
+ const t = getAgentTrustCached(target);
30
+ if (!t)
31
+ return void res.status(404).json({ error: 'agent_not_found' });
32
+ res.json(t);
33
+ });
34
+ }
@@ -0,0 +1,101 @@
1
+ export function registerAiRoutes(app, deps) {
2
+ const { db, auth, anthropic } = deps;
3
+ // G-2: AI 价格建议
4
+ app.post('/api/ai/price-suggestion', async (req, res) => {
5
+ const user = auth(req, res);
6
+ if (!user)
7
+ return;
8
+ if (user.role !== 'seller')
9
+ return void res.status(403).json({ error: '仅卖家可用' });
10
+ const { title, category, description } = req.body || {};
11
+ if (!title)
12
+ return void res.status(400).json({ error: '请提供 title' });
13
+ // 类目历史价位
14
+ const stats = db.prepare(`
15
+ SELECT COUNT(*) as cnt, COALESCE(AVG(price), 0) as avg, COALESCE(MIN(price), 0) as min, COALESCE(MAX(price), 0) as max,
16
+ COALESCE((SELECT price FROM products WHERE status='active' AND category = ? ORDER BY price LIMIT 1 OFFSET CAST((SELECT COUNT(*) FROM products WHERE status='active' AND category = ?) / 2 AS INTEGER)), 0) as median
17
+ FROM products WHERE status = 'active' AND category = ?
18
+ `).get(category || '', category || '', category || '');
19
+ // 近 30 天成交均价(更可信)
20
+ const recentAvg = db.prepare(`
21
+ SELECT COALESCE(AVG(total_amount), 0) as avg FROM orders o
22
+ JOIN products p ON p.id = o.product_id
23
+ WHERE p.category = ? AND o.status = 'completed' AND o.created_at > datetime('now', '-30 days')
24
+ `).get(category || '').avg;
25
+ try {
26
+ const message = await anthropic.messages.create({
27
+ model: 'claude-haiku-4-5-20251001',
28
+ max_tokens: 400,
29
+ messages: [{
30
+ role: 'user',
31
+ content: `你是 WebAZ 定价顾问。给以下商品建议合理定价(WAZ ≈ CNY,1 USDC ≈ 1 WAZ):
32
+ 商品标题: ${String(title).slice(0, 100)}
33
+ 类目: ${category || '未填'}
34
+ 描述: ${String(description || '').slice(0, 300)}
35
+
36
+ 同类目市场数据(active 商品):
37
+ - 商品数: ${stats.cnt}
38
+ - 价位区间: ${stats.min} - ${stats.max} WAZ
39
+ - 均价: ${stats.avg.toFixed(0)} WAZ
40
+ - 中位价: ${stats.median} WAZ
41
+ - 近 30 天成交均价: ${recentAvg.toFixed(0)} WAZ
42
+
43
+ 只返回 JSON(无前后缀):
44
+ {
45
+ "suggested_price": 推荐价数字,
46
+ "low_price": 价格区间下限,
47
+ "high_price": 价格区间上限,
48
+ "reasoning": "1-2 句简短解释"
49
+ }`,
50
+ }],
51
+ });
52
+ const text = message.content[0]?.text || '';
53
+ const m = text.match(/\{[\s\S]*\}/);
54
+ if (!m)
55
+ return void res.status(500).json({ error: 'AI 返回格式错误' });
56
+ const parsed = JSON.parse(m[0]);
57
+ res.json({ ...parsed, market_data: stats, recent_avg: recentAvg });
58
+ }
59
+ catch (e) {
60
+ res.status(503).json({ error: 'AI 失败: ' + e.message });
61
+ }
62
+ });
63
+ // G-1: AI 文案生成(卖家发品辅助)
64
+ app.post('/api/ai/generate-description', async (req, res) => {
65
+ const user = auth(req, res);
66
+ if (!user)
67
+ return;
68
+ if (user.role !== 'seller')
69
+ return void res.status(403).json({ error: '仅卖家可用' });
70
+ const { title, category, keywords, language } = req.body || {};
71
+ if (!title)
72
+ return void res.status(400).json({ error: '请提供 title' });
73
+ const lang = language === 'en' ? 'English' : '中文';
74
+ try {
75
+ const message = await anthropic.messages.create({
76
+ model: 'claude-haiku-4-5-20251001',
77
+ max_tokens: 600,
78
+ messages: [{
79
+ role: 'user',
80
+ content: `你是 WebAZ 电商文案助手。根据以下信息生成商品描述(${lang}):
81
+ - 标题: ${String(title).slice(0, 100)}
82
+ - 类目: ${category || '未填'}
83
+ - 关键词: ${(keywords || []).slice(0, 10).join('、') || '无'}
84
+
85
+ 要求:
86
+ 1. 100-200 字
87
+ 2. 强调 1-2 个差异化卖点
88
+ 3. 无虚假宣传 / 无绝对化用语(最、第一)
89
+ 4. ${lang}
90
+ 5. 不加 emoji
91
+ 6. 直接输出文案正文,无多余前后缀`,
92
+ }],
93
+ });
94
+ const text = message.content[0]?.text || '';
95
+ res.json({ description: text.trim(), model: 'claude-haiku-4-5' });
96
+ }
97
+ catch (e) {
98
+ res.status(503).json({ error: 'AI 生成失败: ' + e.message });
99
+ }
100
+ });
101
+ }