@seasonkoh/webaz 0.1.28 → 0.1.30

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 (198) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/bond-refund-blockers.js +50 -0
  4. package/dist/bond-slash.js +100 -0
  5. package/dist/bond-terms.js +31 -0
  6. package/dist/currency.js +16 -0
  7. package/dist/deposit-rails.js +52 -0
  8. package/dist/direct-pay-aml-monitor.js +67 -0
  9. package/dist/direct-pay-aml-review.js +40 -0
  10. package/dist/direct-pay-base-bond-entry.js +5 -0
  11. package/dist/direct-pay-bond-rail-clearance.js +99 -0
  12. package/dist/direct-pay-cancel-refund.js +160 -0
  13. package/dist/direct-pay-compliance-ingress.js +145 -0
  14. package/dist/direct-pay-controls.js +136 -0
  15. package/dist/direct-pay-create.js +231 -0
  16. package/dist/direct-pay-deferral-quota.js +43 -0
  17. package/dist/direct-pay-disclosures.js +50 -0
  18. package/dist/direct-pay-eligibility.js +46 -0
  19. package/dist/direct-pay-fee-ar.js +241 -0
  20. package/dist/direct-pay-fee-prepay-request.js +80 -0
  21. package/dist/direct-pay-launch-readiness.js +108 -0
  22. package/dist/direct-pay-launch-summary.js +68 -0
  23. package/dist/direct-pay-ledger.js +94 -0
  24. package/dist/direct-pay-returns.js +104 -0
  25. package/dist/direct-pay-stock.js +9 -0
  26. package/dist/direct-receive-account-qr.js +77 -0
  27. package/dist/direct-receive-accounts.js +82 -0
  28. package/dist/direct-receive-deferral.js +171 -0
  29. package/dist/direct-receive-deposits.js +365 -0
  30. package/dist/direct-receive-payment-instruction.js +26 -0
  31. package/dist/free-shipping.js +37 -0
  32. package/dist/fx-rates.js +78 -0
  33. package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
  34. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  35. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  36. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
  37. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  38. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  39. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  40. package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
  41. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  42. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  43. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  44. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  45. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  46. package/dist/ledger.js +12 -3
  47. package/dist/mcp.js +23 -4
  48. package/dist/merchant-bond-domain.js +64 -0
  49. package/dist/merchant-bond-exposure.js +78 -0
  50. package/dist/merchant-bond-watcher.js +26 -0
  51. package/dist/payment-rails.js +29 -0
  52. package/dist/platform-receive-accounts.js +94 -0
  53. package/dist/product-verification.js +93 -0
  54. package/dist/pwa/acp-feed.js +3 -2
  55. package/dist/pwa/arbitration-read-admin.js +37 -0
  56. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  57. package/dist/pwa/contract-fingerprint.js +18 -0
  58. package/dist/pwa/direct-pay-guards.js +20 -0
  59. package/dist/pwa/direct-pay-order-redaction.js +42 -0
  60. package/dist/pwa/endpoint-actions.js +3 -0
  61. package/dist/pwa/human-presence.js +2 -6
  62. package/dist/pwa/public/app-account.js +9 -9
  63. package/dist/pwa/public/app-admin-disputes.js +55 -0
  64. package/dist/pwa/public/app-agent-appeal.js +90 -0
  65. package/dist/pwa/public/app-agent-approvals.js +93 -0
  66. package/dist/pwa/public/app-agent-pair.js +127 -0
  67. package/dist/pwa/public/app-ai.js +10 -10
  68. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  69. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  70. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  71. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  72. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  73. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  74. package/dist/pwa/public/app-bond-ui.js +108 -0
  75. package/dist/pwa/public/app-chat-poll.js +29 -0
  76. package/dist/pwa/public/app-contribution-hub.js +23 -0
  77. package/dist/pwa/public/app-create-kinds.js +17 -0
  78. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  79. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  80. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  81. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  82. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  83. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  84. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  85. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  86. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  87. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  88. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  89. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  90. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  91. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  92. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  93. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  94. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  95. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  96. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  97. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  98. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  99. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  100. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  101. package/dist/pwa/public/app-direct-pay.js +226 -0
  102. package/dist/pwa/public/app-discover.js +20 -20
  103. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  104. package/dist/pwa/public/app-external-links.js +32 -0
  105. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  106. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  107. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  108. package/dist/pwa/public/app-listings.js +4 -4
  109. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  110. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  111. package/dist/pwa/public/app-notif-templates.js +22 -0
  112. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  113. package/dist/pwa/public/app-order-errors.js +50 -0
  114. package/dist/pwa/public/app-order-labels.js +19 -0
  115. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  116. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  117. package/dist/pwa/public/app-poll-governor.js +22 -0
  118. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  119. package/dist/pwa/public/app-price.js +55 -0
  120. package/dist/pwa/public/app-product-media.js +15 -0
  121. package/dist/pwa/public/app-profile.js +10 -10
  122. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  123. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  124. package/dist/pwa/public/app-seller.js +5 -5
  125. package/dist/pwa/public/app-shop.js +19 -19
  126. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  127. package/dist/pwa/public/app.js +264 -295
  128. package/dist/pwa/public/i18n.js +1068 -197
  129. package/dist/pwa/public/index.html +58 -0
  130. package/dist/pwa/public/openapi.json +1845 -653
  131. package/dist/pwa/public/style.css +3 -0
  132. package/dist/pwa/routes/admin-analytics.js +4 -2
  133. package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
  134. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  135. package/dist/pwa/routes/admin-reports.js +31 -5
  136. package/dist/pwa/routes/agent-governance.js +1 -1
  137. package/dist/pwa/routes/agent-grants.js +253 -32
  138. package/dist/pwa/routes/analytics.js +2 -0
  139. package/dist/pwa/routes/arbitrator.js +67 -14
  140. package/dist/pwa/routes/bond-seller.js +162 -0
  141. package/dist/pwa/routes/buyer-feeds.js +2 -1
  142. package/dist/pwa/routes/chat.js +6 -1
  143. package/dist/pwa/routes/dashboards.js +2 -1
  144. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  145. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  146. package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
  147. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  148. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  149. package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
  150. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  151. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  152. package/dist/pwa/routes/disputes-read.js +20 -6
  153. package/dist/pwa/routes/disputes-write.js +91 -33
  154. package/dist/pwa/routes/external-anchors.js +4 -2
  155. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  156. package/dist/pwa/routes/fx.js +12 -0
  157. package/dist/pwa/routes/governance-onboarding.js +46 -8
  158. package/dist/pwa/routes/leaderboard.js +47 -9
  159. package/dist/pwa/routes/listings.js +2 -2
  160. package/dist/pwa/routes/logistics.js +6 -2
  161. package/dist/pwa/routes/manifests.js +38 -0
  162. package/dist/pwa/routes/me-data.js +5 -2
  163. package/dist/pwa/routes/mutual-cancel.js +62 -0
  164. package/dist/pwa/routes/orders-action.js +297 -16
  165. package/dist/pwa/routes/orders-create.js +21 -6
  166. package/dist/pwa/routes/orders-read.js +106 -8
  167. package/dist/pwa/routes/p2p-products.js +2 -2
  168. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  169. package/dist/pwa/routes/products-create.js +15 -4
  170. package/dist/pwa/routes/products-links.js +34 -0
  171. package/dist/pwa/routes/products-list.js +2 -1
  172. package/dist/pwa/routes/products-update.js +37 -3
  173. package/dist/pwa/routes/profile-identity.js +4 -2
  174. package/dist/pwa/routes/promoter.js +3 -0
  175. package/dist/pwa/routes/referral.js +4 -0
  176. package/dist/pwa/routes/returns.js +60 -4
  177. package/dist/pwa/routes/rewards-apply.js +10 -5
  178. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  179. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  180. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  181. package/dist/pwa/routes/seller-quota.js +5 -1
  182. package/dist/pwa/routes/shipping-templates.js +219 -0
  183. package/dist/pwa/routes/shops.js +2 -1
  184. package/dist/pwa/routes/snf.js +4 -1
  185. package/dist/pwa/routes/url-claim.js +2 -2
  186. package/dist/pwa/routes/users-public.js +8 -0
  187. package/dist/pwa/routes/wallet-read.js +3 -0
  188. package/dist/pwa/routes/webauthn.js +3 -3
  189. package/dist/pwa/server.js +112 -132
  190. package/dist/runtime/agent-grant-scopes.js +69 -1
  191. package/dist/runtime/webaz-schema-helpers.js +161 -3
  192. package/dist/sale-regions.js +116 -0
  193. package/dist/shipping-templates.js +119 -0
  194. package/dist/store-verification.js +77 -0
  195. package/dist/trade-tax.js +99 -0
  196. package/dist/trade-terms.js +76 -0
  197. package/dist/version.js +1 -1
  198. package/package.json +128 -2
@@ -6,20 +6,29 @@ export function registerAdminReportsRoutes(app, deps) {
6
6
  const admin = requireContentAdmin(req, res);
7
7
  if (!admin)
8
8
  return;
9
+ // 审计项 G:补 payment_rail 列 + ?rail 过滤 —— admin/AML 此前在订单总览里区分不出 direct_p2p(非托管轨监控盲区)
9
10
  const status = req.query.status;
11
+ const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
10
12
  let sql = `SELECT o.id, o.product_id, o.buyer_id, o.seller_id, o.logistics_id, o.status,
11
- o.total_amount, o.created_at,
13
+ o.total_amount, o.created_at, o.payment_rail,
12
14
  p.title as product_title,
13
15
  ub.name as buyer_name, us.name as seller_name
14
16
  FROM orders o
15
17
  JOIN products p ON o.product_id = p.id
16
18
  JOIN users ub ON o.buyer_id = ub.id
17
19
  JOIN users us ON o.seller_id = us.id`;
20
+ const where = [];
18
21
  const params = [];
19
22
  if (status && status.trim()) {
20
- sql += ` WHERE o.status = ?`;
23
+ where.push('o.status = ?');
21
24
  params.push(status);
22
25
  }
26
+ if (rail) {
27
+ where.push('o.payment_rail = ?');
28
+ params.push(rail);
29
+ }
30
+ if (where.length)
31
+ sql += ' WHERE ' + where.join(' AND ');
23
32
  sql += ` ORDER BY o.created_at DESC LIMIT 100`;
24
33
  res.json({ orders: await dbAll(sql, params) });
25
34
  });
@@ -27,20 +36,37 @@ export function registerAdminReportsRoutes(app, deps) {
27
36
  const admin = requireArbitrationAdmin(req, res);
28
37
  if (!admin)
29
38
  return;
39
+ // 区分:按 status(open/in_review/resolved/dismissed)+ rail(direct_p2p/escrow)过滤;附 verdict/ruling_type/
40
+ // assigned_arbitrators/payment_rail 让 admin 分辨类型/进度/是否已指派/如何结案。summary 给全量分状态计数。
41
+ const status = typeof req.query.status === 'string' ? req.query.status.trim() : '';
42
+ const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
43
+ const where = [];
44
+ const params = [];
45
+ if (status) {
46
+ where.push('d.status = ?');
47
+ params.push(status);
48
+ }
49
+ if (rail) {
50
+ where.push('o.payment_rail = ?');
51
+ params.push(rail);
52
+ }
30
53
  const rows = await dbAll(`
31
54
  SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status,
32
55
  d.created_at, d.respond_deadline, d.arbitrate_deadline, d.resolved_at,
56
+ d.verdict, d.ruling_type, d.assigned_arbitrators,
33
57
  u1.name as initiator_name, u2.name as defendant_name,
34
- o.total_amount, o.status as order_status,
58
+ o.total_amount, o.status as order_status, o.payment_rail,
35
59
  p.title as product_title
36
60
  FROM disputes d
37
61
  LEFT JOIN users u1 ON d.initiator_id = u1.id
38
62
  LEFT JOIN users u2 ON d.defendant_id = u2.id
39
63
  LEFT JOIN orders o ON d.order_id = o.id
40
64
  LEFT JOIN products p ON o.product_id = p.id
65
+ ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
41
66
  ORDER BY d.created_at DESC LIMIT 100
42
- `);
43
- res.json({ disputes: rows });
67
+ `, params);
68
+ const counts = await dbAll('SELECT status, COUNT(*) n FROM disputes GROUP BY status');
69
+ res.json({ disputes: rows, counts: Object.fromEntries(counts.map(c => [c.status, c.n])), total: counts.reduce((s, c) => s + c.n, 0) });
44
70
  });
45
71
  app.get('/api/admin/verify-tasks', async (req, res) => {
46
72
  const admin = requireArbitrationAdmin(req, res);
@@ -22,7 +22,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
22
22
  }
23
23
  const calls30d = (await dbOne(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`, [k.api_key])).n;
24
24
  const last = await dbOne(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`, [k.api_key]);
25
- const strikes = await dbAll(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]);
25
+ const strikes = await dbAll(`SELECT id, severity, reason_code, reason_detail, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]); // +id/detail:被封申诉 UI 需要(此端点封禁豁免)
26
26
  let passport = null;
27
27
  try {
28
28
  passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
@@ -1,12 +1,16 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
3
- import { initAgentDelegationGrantsSchema, initAgentPairingSchema, initAgentGrantAuthLogSchema } from '../../runtime/webaz-schema-helpers.js';
4
- import { validateRequestedCapabilities, clampTtlSeconds, grantIsActive } from '../../runtime/agent-grant-scopes.js';
3
+ import { initAgentDelegationGrantsSchema, initAgentPairingSchema, initAgentGrantAuthLogSchema, initAgentPermissionRequestsSchema } from '../../runtime/webaz-schema-helpers.js';
4
+ import { validateRequestedCapabilities, clampTtlSeconds, grantIsActive, resolveBundle, durationAllowedForScopes, suggestedDurationForScopes, durationToSeconds, riskLevelForScopes } from '../../runtime/agent-grant-scopes.js';
5
5
  import { generateUserCode, verifyPkceS256, clampPairingTtlSeconds, pairingApprovable, pairingRetrievable } from '../../runtime/agent-pairing.js';
6
6
  import { verifyGrantToken } from '../../runtime/agent-grant-verifier.js';
7
7
  // Bounds on a pairing request (anti-bloat for the anonymous start endpoint).
8
8
  const MAX_CAPABILITIES = 12;
9
9
  const MAX_CONSTRAINTS_JSON = 2000;
10
+ // Thrown inside the approve transaction when the grant is no longer active (revoked/expired in the race
11
+ // window) — rolls the whole tx back so the request claim + expansion + audit are all-or-nothing.
12
+ class GrantInactiveError extends Error {
13
+ }
10
14
  function safeParseCaps(json) {
11
15
  try {
12
16
  return JSON.parse(String(json));
@@ -28,11 +32,23 @@ function consentView(p) {
28
32
  };
29
33
  }
30
34
  export function registerAgentGrantsRoutes(app, deps) {
31
- const { db, auth, generateId, rateLimitOk } = deps;
35
+ const { db, auth, generateId, rateLimitOk, requireHumanPresence } = deps;
32
36
  // PWA runtime self-init (MCP gets the tables via applyWebazRuntimeSchema). Idempotent.
33
37
  initAgentDelegationGrantsSchema(db);
34
38
  initAgentPairingSchema(db);
35
39
  initAgentGrantAuthLogSchema(db);
40
+ initAgentPermissionRequestsSchema(db);
41
+ // Resolve the ACTIVE grant behind a gtk_ bearer (no scope check) — used to bind a permission request to
42
+ // (grant_id, human_id). Returns null on missing/expired/revoked. token_hash lookup mirrors the verifier.
43
+ async function resolveActiveGrantByBearer(req) {
44
+ const bearer = (req.header('authorization') || '').replace(/^Bearer\s+/i, '');
45
+ if (!bearer.startsWith('gtk_'))
46
+ return null;
47
+ const g = await dbOne('SELECT grant_id, human_id, agent_label, capabilities, status, expires_at, revoked_at FROM agent_delegation_grants WHERE token_hash = ?', [createHash('sha256').update(bearer).digest('hex')]);
48
+ if (!g || !grantIsActive(g, new Date().toISOString()))
49
+ return null;
50
+ return { grant_id: g.grant_id, human_id: g.human_id, agent_label: g.agent_label, capabilities: g.capabilities };
51
+ }
36
52
  // ─────────────────────────── RFC-020 PR-C2a: opt-in grant-scope enforcement ───────────────────────────
37
53
  // EXPLICIT, per-route, per-SAFE-scope. NOT global auth — a gtk_* token is accepted ONLY by routes that
38
54
  // deliberately mount requireAgentGrantScope(scope); auth()/api_key is untouched and never accepts gtk_*.
@@ -60,8 +76,23 @@ export function registerAgentGrantsRoutes(app, deps) {
60
76
  }
61
77
  }
62
78
  // Deny path: return the denial regardless of audit (no access is granted, so nothing to fail closed on).
63
- if (!r.ok)
79
+ if (!r.ok) {
80
+ // Structured permission_required (RFC-020): the agent IS validly connected but its grant simply lacks
81
+ // this SAFE scope. Instead of a bare 403, hand it the exact next step — ask the human to expand the
82
+ // grant (approval_url + the create-request call), then retry this same request. Other grant failures
83
+ // (no/expired/revoked/suspended grant) stay plain: those aren't "request more", they must re-pair.
84
+ if (r.error_code === 'SCOPE_NOT_GRANTED') {
85
+ return void res.status(403).json({
86
+ error: `this action needs the "${scope}" permission, which your grant does not carry`,
87
+ error_code: 'PERMISSION_REQUIRED',
88
+ required_scope: scope,
89
+ approval_url: '/#agent-approvals',
90
+ request_permission: { method: 'POST', endpoint: '/api/agent-grants/permission-requests', body: { scopes: [scope] } },
91
+ note: 'Ask the human to approve at approval_url; on approval your existing grant is expanded — then retry this request.',
92
+ });
93
+ }
64
94
  return void res.status(r.status).json({ error: r.error, error_code: r.error_code });
95
+ }
65
96
  // Success path: FAIL CLOSED if the authorization could not be audited — a grant-authorized request
66
97
  // must never proceed unaudited (RFC-020 invariant). Better to deny (503, retryable) than act unaccountably.
67
98
  if (!audited)
@@ -75,6 +106,185 @@ export function registerAgentGrantsRoutes(app, deps) {
75
106
  const p = req.agentGrant;
76
107
  res.json({ grant: p, note: 'Authorized via delegation grant (safe scope read_public). This is a grant principal, not a human session.' });
77
108
  });
109
+ // First REAL grant-consumed seller surface (Catalog Agent): read the grant human's OWN catalog. Read-only,
110
+ // money fields (commission/stake) excluded. A grant that lacks seller_products_read → structured
111
+ // permission_required (see requireAgentGrantScope) so the agent can request → human approves → retry.
112
+ // The consumption (allow AND the permission_required deny) is audited by the middleware.
113
+ app.get('/api/agent/seller/products', requireAgentGrantScope('seller_products_read'), async (req, res) => {
114
+ const p = req.agentGrant;
115
+ const rows = await dbAll("SELECT id, title, status, price, currency, stock, category, created_at, updated_at FROM products WHERE seller_id = ? AND status != 'deleted' ORDER BY created_at DESC LIMIT 200", [p.human_id]);
116
+ res.json({ seller_id: p.human_id, agent_label: p.agent_label, count: rows.length, products: rows, note: 'Seller-owned catalog read via delegation grant (safe scope seller_products_read). Read-only; no money/commission fields.' });
117
+ });
118
+ // helpers for the permission-request flow ----------------------------------------------------------------
119
+ const parseCapList = (json) => { try {
120
+ const a = JSON.parse(json);
121
+ return Array.isArray(a) ? a : [];
122
+ }
123
+ catch {
124
+ return [];
125
+ } };
126
+ const scopeNames = (json) => parseCapList(json).map(c => (typeof c === 'string' ? c : c?.capability)).filter((s) => typeof s === 'string');
127
+ // Returns true iff the audit row was durably written. Callers on the grant-authorized SUCCESS path MUST
128
+ // fail closed when this is false (RFC-020 invariant: a grant-authorized action is audited or it does not
129
+ // happen) — parity with the requireAgentGrantScope middleware above.
130
+ const auditGrant = async (grantId, humanId, cap, outcome, errorCode) => {
131
+ try {
132
+ await dbRun('INSERT INTO agent_grant_auth_log (grant_id, human_id, capability, outcome, error_code) VALUES (?,?,?,?,?)', [grantId, humanId, cap, outcome, errorCode ?? null]);
133
+ return true;
134
+ }
135
+ catch (e) {
136
+ console.error('[agent-grant] audit write failed:', e.message);
137
+ return false;
138
+ }
139
+ };
140
+ const bundleSummary = (key) => { const b = key ? resolveBundle(key) : null; return b ? b.human_summary : null; };
141
+ // GET verify — grant-authed. Returns the FULL grant (scopes, bundle, expiry, status), not just read_public.
142
+ // Audited (acceptance #8: every grant use logs). Never returns the raw token/api_key.
143
+ app.get('/api/agent-grants/verify', async (req, res) => {
144
+ const g = await resolveActiveGrantByBearer(req);
145
+ if (!g) {
146
+ return void res.status(401).json({ error: 'active delegation grant required', error_code: 'GRANT_REQUIRED' });
147
+ }
148
+ const full = await dbOne('SELECT grant_id, human_id, agent_label, capabilities, status, expires_at, permission_bundle FROM agent_delegation_grants WHERE grant_id = ?', [g.grant_id]);
149
+ // Fail closed: a grant-authorized read is audited or it does not proceed (parity with requireAgentGrantScope).
150
+ if (!(await auditGrant(g.grant_id, g.human_id, 'grant:verify', 'allow')))
151
+ return void res.status(503).json({ error: 'authorization audit unavailable; refusing to proceed unaudited', error_code: 'GRANT_AUDIT_FAILED' });
152
+ res.json({ grant: { grant_id: full.grant_id, agent_label: full.agent_label, scopes: scopeNames(full.capabilities), permission_bundle: full.permission_bundle, expires_at: full.expires_at, status: full.status }, note: 'Full grant principal — scopes/bundle/expiry/status. Not a human session; never authorizes risk/never-delegable actions.' });
153
+ });
154
+ // POST create a permission request — the AGENT (holding its current grant) asks for MORE scope / a bundle.
155
+ // Bound to (human_id, grant_id) from the grant bearer. Rate-limited. Safe-only: risk/never-delegable are
156
+ // NOT grantable (they need a per-action live Passkey, not a persistent grant) → structured reject.
157
+ app.post('/api/agent-grants/permission-requests', async (req, res) => {
158
+ if (!rateLimitOk(`agent_perm_req:${req.ip || 'anon'}`, 20, 60_000))
159
+ return void res.status(429).json({ error: 'too_many_permission_requests', retry_after_s: 60 });
160
+ const g = await resolveActiveGrantByBearer(req);
161
+ if (!g)
162
+ return void res.status(401).json({ error: 'an active delegation grant is required to request more permissions (pair first with webaz_pair)', error_code: 'GRANT_REQUIRED' });
163
+ const body = (req.body || {});
164
+ const bundleKey = typeof body.bundle === 'string' ? body.bundle : null;
165
+ const bundle = bundleKey ? resolveBundle(bundleKey) : null;
166
+ if (bundleKey && !bundle)
167
+ return void res.status(400).json({ error: 'unknown permission bundle', error_code: 'UNKNOWN_BUNDLE' });
168
+ const scopes = bundle ? [...bundle.scopes] : (Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === 'string') : []);
169
+ if (scopes.length === 0)
170
+ return void res.status(400).json({ error: 'bundle or scopes required', error_code: 'NO_SCOPES' });
171
+ const v = validateRequestedCapabilities(scopes.map(s => ({ capability: s })));
172
+ if (!v.ok)
173
+ return void res.status(403).json({ error: 'permission_not_grantable', error_code: 'PERMISSION_NOT_GRANTABLE', rejected: v.rejected, note: 'Only safe (read/draft) scopes can be granted. Risk actions (order/publish/refund/…) require the human to act with a live Passkey — they are never delegated to a persistent grant.' });
174
+ const risk = riskLevelForScopes(scopes);
175
+ const duration = durationAllowedForScopes(scopes, body.duration) ? body.duration : suggestedDurationForScopes(scopes);
176
+ const id = generateId('apr');
177
+ const reqTtlIso = new Date(Date.now() + 7 * 86400_000).toISOString(); // request auto-expires in 7d if unanswered
178
+ await dbRun('INSERT INTO agent_permission_requests (id, human_id, grant_id, agent_label, requested_scopes, permission_bundle, reason, task_context, risk_level, duration, status, expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [id, g.human_id, g.grant_id, g.agent_label, JSON.stringify(scopes), bundleKey, typeof body.reason === 'string' ? body.reason.slice(0, 280) : null, typeof body.task_context === 'string' ? body.task_context.slice(0, 500) : null, risk, duration, 'pending', reqTtlIso]);
179
+ res.status(201).json({ approval_id: id, approval_url: '/#agent-approvals', status: 'pending', risk_level: risk, requested_scopes: scopes, permission_bundle: bundleKey, human_summary: bundleSummary(bundleKey), suggested_duration: duration, note: 'Ask the human to open approve_url (logged in) and approve. On approval your existing grant is expanded; then retry.' });
180
+ });
181
+ // GET list this human's PENDING permission requests (for #agent-approvals). Human-authed.
182
+ app.get('/api/agent-grants/permission-requests', async (req, res) => {
183
+ const user = auth(req, res);
184
+ if (!user)
185
+ return;
186
+ const rows = await dbAll("SELECT id, agent_label, requested_scopes, permission_bundle, reason, task_context, risk_level, duration, created_at, expires_at FROM agent_permission_requests WHERE human_id = ? AND status = 'pending' AND expires_at > ? ORDER BY created_at DESC LIMIT 100", [user.id, new Date().toISOString()]);
187
+ res.json({ requests: rows.map(r => ({ ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) })) });
188
+ });
189
+ // GET list the requests THIS grant created — GRANT-authed (the agent, via webaz_pair), so an agent can poll
190
+ // its own request status (pending/approved/rejected/expired) without hitting the target surface. Bound to
191
+ // grant_id: an agent sees ONLY its own requests, never the human's other agents'. Audited (fail-closed).
192
+ app.get('/api/agent-grants/my-permission-requests', async (req, res) => {
193
+ if (!rateLimitOk(`agent_perm_list:${req.ip || 'anon'}`, 30, 60_000))
194
+ return void res.status(429).json({ error: 'too_many_requests', error_code: 'GRANT_RATE_LIMITED', retry_after_s: 60 });
195
+ const g = await resolveActiveGrantByBearer(req);
196
+ if (!g)
197
+ return void res.status(401).json({ error: 'an active delegation grant is required (pair first with webaz_pair)', error_code: 'GRANT_REQUIRED' });
198
+ const rows = await dbAll('SELECT id, requested_scopes, permission_bundle, risk_level, duration, status, created_at, expires_at, approved_at FROM agent_permission_requests WHERE grant_id = ? ORDER BY created_at DESC LIMIT 50', [g.grant_id]);
199
+ if (!(await auditGrant(g.grant_id, g.human_id, 'grant:list_requests', 'allow')))
200
+ return void res.status(503).json({ error: 'authorization audit unavailable; refusing to proceed unaudited', error_code: 'GRANT_AUDIT_FAILED' });
201
+ res.json({ requests: rows.map(r => ({ ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) })) });
202
+ });
203
+ // RFC-020: expanding an agent grant is a privilege escalation (like initial pairing) — a stolen web session
204
+ // must NOT widen an agent from read_public to a long-term bundle. So a LIVE Passkey bound to this request_id
205
+ // is required. Grant-active is checked BEFORE claiming the request, and the expand is guarded + reversible,
206
+ // so a mid-flight expire/revoke can never strand a phantom 'approved'.
207
+ // POST approve — human-authed + live Passkey; expands the bound grant (union scopes + bundle + extend expiry). Audited.
208
+ app.post('/api/agent-grants/permission-requests/:id/approve', async (req, res) => {
209
+ const user = auth(req, res);
210
+ if (!user)
211
+ return;
212
+ const now = new Date().toISOString();
213
+ const r = await dbOne('SELECT human_id, grant_id, requested_scopes, permission_bundle, duration, status, expires_at FROM agent_permission_requests WHERE id = ?', [req.params.id]);
214
+ if (!r)
215
+ return void res.status(404).json({ error: 'permission_request_not_found' });
216
+ if (r.human_id !== user.id)
217
+ return void res.status(403).json({ error: 'not your permission request' });
218
+ if (r.status !== 'pending' || r.expires_at <= now)
219
+ return void res.status(409).json({ error: 'permission_request_not_pending', status: r.status });
220
+ // Live Passkey, bound to THIS request (a token minted for request A can't approve B).
221
+ const hp = requireHumanPresence(user.id, 'agent_permission_approve', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_permission_approve', (data) => { try {
222
+ return typeof data === 'object' && data !== null && data.request_id === req.params.id;
223
+ }
224
+ catch {
225
+ return false;
226
+ } });
227
+ if (!hp.ok)
228
+ return void res.status(412).json({ error: hp.reason, error_code: hp.error_code });
229
+ const reqScopes = scopeNames(r.requested_scopes);
230
+ // Defense in depth: re-validate safe-only + duration allowed at approval time.
231
+ if (!validateRequestedCapabilities(reqScopes.map(s => ({ capability: s }))).ok)
232
+ return void res.status(403).json({ error: 'permission_not_grantable', error_code: 'PERMISSION_NOT_GRANTABLE' });
233
+ if (!durationAllowedForScopes(reqScopes, r.duration))
234
+ return void res.status(403).json({ error: 'duration_not_allowed_for_risk', error_code: 'DURATION_NOT_ALLOWED' });
235
+ // (P2) Verify the grant is ACTIVE *before* claiming the request — never leave a phantom 'approved'.
236
+ const grant = await dbOne('SELECT grant_id, capabilities, status, expires_at, revoked_at FROM agent_delegation_grants WHERE grant_id = ?', [r.grant_id]);
237
+ if (!grant || !grantIsActive(grant, now))
238
+ return void res.status(409).json({ error: 'grant_inactive', error_code: 'GRANT_INACTIVE', note: 'the agent grant expired or was revoked; re-pair (this request stays pending)' });
239
+ // Union new scopes into the grant; set bundle; extend expiry (never shorten). 'once' → short 1h window.
240
+ const union = [...new Set([...scopeNames(grant.capabilities), ...reqScopes])].map(s => ({ capability: s, constraints: {} }));
241
+ const secs = durationToSeconds(r.duration) || 3600;
242
+ const newExpiry = new Date(Date.now() + secs * 1000).toISOString();
243
+ const expiresAt = newExpiry > grant.expires_at ? newExpiry : grant.expires_at;
244
+ // ATOMIC (RFC-020 invariant: a grant expansion is audited-or-it-does-not-happen; parity with arbitrator
245
+ // approve). CAS-claim the request + guarded-expand the grant + write the audit row in ONE sync
246
+ // db.transaction. If the audit INSERT throws, OR the grant was revoked in the race window (guarded
247
+ // WHERE → 0 rows → GrantInactiveError), the WHOLE tx rolls back: scopes unchanged, request stays pending.
248
+ let outcome;
249
+ try {
250
+ outcome = db.transaction(() => {
251
+ const claimed = db.prepare("UPDATE agent_permission_requests SET status='approved', approved_at=? WHERE id=? AND status='pending'").run(now, req.params.id);
252
+ if (claimed.changes !== 1)
253
+ return 'not_pending';
254
+ const expanded = db.prepare("UPDATE agent_delegation_grants SET capabilities=?, permission_bundle=COALESCE(?, permission_bundle), expires_at=? WHERE grant_id=? AND status='active' AND revoked_at IS NULL").run(JSON.stringify(union), r.permission_bundle, expiresAt, grant.grant_id);
255
+ if (expanded.changes !== 1)
256
+ throw new GrantInactiveError();
257
+ db.prepare('INSERT INTO agent_grant_auth_log (grant_id, human_id, capability, outcome, error_code) VALUES (?,?,?,?,?)').run(grant.grant_id, user.id, `permission_request:approve:${r.permission_bundle || reqScopes.join(',')}`, 'allow', null);
258
+ return 'expanded';
259
+ })();
260
+ }
261
+ catch (e) {
262
+ if (e instanceof GrantInactiveError)
263
+ return void res.status(409).json({ error: 'grant_inactive', error_code: 'GRANT_INACTIVE', note: 'the agent grant was revoked while approving; request stays pending' });
264
+ console.error('[agent-grant] approve tx failed (audit unavailable?):', e.message);
265
+ return void res.status(503).json({ error: 'authorization audit unavailable; refusing to expand unaudited', error_code: 'GRANT_AUDIT_FAILED' });
266
+ }
267
+ if (outcome === 'not_pending')
268
+ return void res.status(409).json({ error: 'permission_request_not_pending' });
269
+ res.json({ success: true, grant_id: grant.grant_id, scopes: union.map(u => u.capability), permission_bundle: r.permission_bundle, expires_at: expiresAt });
270
+ });
271
+ // POST reject — human-authed. Terminal 'rejected'; nothing is granted.
272
+ app.post('/api/agent-grants/permission-requests/:id/reject', async (req, res) => {
273
+ const user = auth(req, res);
274
+ if (!user)
275
+ return;
276
+ const r = await dbOne('SELECT human_id, status FROM agent_permission_requests WHERE id = ?', [req.params.id]);
277
+ if (!r)
278
+ return void res.status(404).json({ error: 'permission_request_not_found' });
279
+ if (r.human_id !== user.id)
280
+ return void res.status(403).json({ error: 'not your permission request' });
281
+ if (r.status !== 'pending')
282
+ return void res.status(409).json({ error: 'permission_request_not_pending', status: r.status });
283
+ const rj = await dbRun("UPDATE agent_permission_requests SET status='rejected' WHERE id=? AND status='pending'", [req.params.id]);
284
+ if (!rj || rj.changes !== 1)
285
+ return void res.status(409).json({ error: 'permission_request_not_pending' });
286
+ res.json({ success: true, status: 'rejected' });
287
+ });
78
288
  // ─────────────────────────── RFC-020 PR-C1: pairing (device-flow + PKCE) ───────────────────────────
79
289
  // C1 = pairing + credential delivery ONLY. No grant is consumed by any tool here (that is PR-C2).
80
290
  // (pair 1) Agent starts a pairing — UNAUTHENTICATED (agent has no credential yet). Safe scopes only.
@@ -135,6 +345,17 @@ export function registerAgentGrantsRoutes(app, deps) {
135
345
  return void res.status(404).json({ error: 'pairing_not_found' });
136
346
  if (!pairingApprovable(p, now))
137
347
  return void res.status(409).json({ error: 'pairing_not_pending_or_expired', status: p.status });
348
+ // 人工在场 gate:批准 = 真人 Passkey/WebAuthn 确认(默认必需,param 可关)。挡"账号被静默批准"。
349
+ // 注:这挡的是"是不是真人在批",不挡 illicit-consent(人被骗批错 agent)—— 那由前端强制口令核对 + safe-scope 兜底。
350
+ // token 必须【绑定这个配对码】(purpose_data.user_code === :user_code)—— 防"为批 A 拿到的 token 被首次提交去批 B"(与 delete_passkey 绑 credential_id 同法)。
351
+ const hp = requireHumanPresence(user.id, 'agent_pair_approve', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_pair_approve', (data) => { try {
352
+ return typeof data === 'object' && data !== null && data.user_code === req.params.user_code;
353
+ }
354
+ catch {
355
+ return false;
356
+ } });
357
+ if (!hp.ok)
358
+ return void res.status(412).json({ error: hp.reason, error_code: hp.error_code });
138
359
  // Re-validate scopes at approval time (defense in depth) — must still be safe-only.
139
360
  const caps = safeParseCaps(p.capabilities);
140
361
  const v = validateRequestedCapabilities(caps);
@@ -142,11 +363,31 @@ export function registerAgentGrantsRoutes(app, deps) {
142
363
  return void res.status(403).json({ error: 'pairing_rejected', rejected: v.rejected });
143
364
  const grantId = generateId('grt');
144
365
  const expiresAt = new Date(Date.now() + clampTtlSeconds(undefined) * 1000).toISOString();
366
+ // 先【CAS 抢占】pending 配对(唯一赢家),再插 grant —— 竞态下输家 changes!==1 直接 409、不插任何 grant,
367
+ // 杜绝"插了 grant 但配对更新失败"留下 token_hash NULL 的 orphan grant(污染连接记录)。
368
+ const claimed = await dbRun("UPDATE agent_pairing_sessions SET status='approved', human_id=?, grant_id=?, approved_at=? WHERE user_code=? AND status='pending'", [user.id, grantId, now, req.params.user_code]);
369
+ if (!claimed || claimed.changes !== 1)
370
+ return void res.status(409).json({ error: 'pairing_not_pending_or_expired' });
145
371
  // Grant created WITHOUT a token (token_hash NULL) — the bearer is minted only at retrieval.
146
372
  await dbRun('INSERT INTO agent_delegation_grants (grant_id, human_id, agent_label, capabilities, token_hash, human_confirm_required, status, expires_at) VALUES (?,?,?,?,?,?,?,?)', [grantId, user.id, p.agent_label || null, JSON.stringify(caps), null, 0, 'active', expiresAt]);
147
- await dbRun("UPDATE agent_pairing_sessions SET status='approved', human_id=?, grant_id=?, approved_at=? WHERE user_code=? AND status='pending'", [user.id, grantId, now, req.params.user_code]);
148
373
  res.json({ success: true, grant_id: grantId, capabilities: caps });
149
374
  });
375
+ // (pair 3b) Human rejects — human-authenticated. Terminal 'rejected' → agent's retrieve fails clearly (no silent lingering).
376
+ // 拒绝是保护性动作,无需 Passkey(不签发任何凭证)。幂等:仅 pending 可拒。
377
+ app.post('/api/agent-grants/pair/:user_code/reject', async (req, res) => {
378
+ const user = auth(req, res);
379
+ if (!user)
380
+ return;
381
+ const p = await dbOne('SELECT status FROM agent_pairing_sessions WHERE user_code = ?', [req.params.user_code]);
382
+ if (!p)
383
+ return void res.status(404).json({ error: 'pairing_not_found' });
384
+ if (p.status !== 'pending')
385
+ return void res.status(409).json({ error: 'pairing_not_pending', status: p.status });
386
+ const r = await dbRun("UPDATE agent_pairing_sessions SET status='rejected', human_id=? WHERE user_code=? AND status='pending'", [user.id, req.params.user_code]);
387
+ if (!r || r.changes !== 1)
388
+ return void res.status(409).json({ error: 'pairing_not_pending' });
389
+ res.json({ success: true, status: 'rejected' });
390
+ });
150
391
  // (pair 4) Agent retrieves the credential ONCE via PKCE verifier — UNAUTHENTICATED (PKCE-gated).
151
392
  app.post('/api/agent-grants/pair/:pairing_id/retrieve', async (req, res) => {
152
393
  const now = new Date().toISOString();
@@ -180,36 +421,16 @@ export function registerAgentGrantsRoutes(app, deps) {
180
421
  expires_at: grant.expires_at,
181
422
  });
182
423
  });
183
- // ── Issue a grant (human-authenticated). Safe scopes only; risk/never-delegable rejected. ──
184
- app.post('/api/agent-grants', async (req, res) => {
424
+ // ── Direct grant issuance is DISABLED single blessed path is the Passkey pairing flow. ──
425
+ // 旧的"仅登录即直接 mint 原文 bearer"入口会旁路 #pair 的真人 Passkey 批准,削弱"human Passkey-approves
426
+ // agent delegation"的安全叙事。零消费方(前端/MCP/测试均不用),故降级为不可用,统一走 pairing。
427
+ app.post('/api/agent-grants', (req, res) => {
185
428
  const user = auth(req, res);
186
429
  if (!user)
187
430
  return;
188
- const body = (req.body || {});
189
- const caps = Array.isArray(body.capabilities) ? body.capabilities : [];
190
- const v = validateRequestedCapabilities(caps);
191
- if (!v.ok) {
192
- // Fail-closed: any risk / never-delegable / unknown scope rejects the whole request.
193
- return void res.status(403).json({ error: 'grant_rejected', rejected: v.rejected });
194
- }
195
- const ttl = clampTtlSeconds(body.ttl_seconds);
196
- const grantId = generateId('grt');
197
- const token = `gtk_${randomBytes(32).toString('hex')}`; // bearer — shown once
198
- const tokenHash = createHash('sha256').update(token).digest('hex');
199
- const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
200
- const label = typeof body.agent_label === 'string' ? body.agent_label.slice(0, 120) : null;
201
- const capsJson = JSON.stringify(v.safe.map(c => ({
202
- capability: c,
203
- constraints: (caps.find(x => x?.capability === c)?.constraints) || {},
204
- })));
205
- await dbRun('INSERT INTO agent_delegation_grants (grant_id, human_id, agent_label, capabilities, token_hash, human_confirm_required, status, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [grantId, user.id, label, capsJson, tokenHash, 0, 'active', expiresAt]);
206
- res.status(201).json({
207
- grant_id: grantId,
208
- token,
209
- token_note: 'Shown once — store securely. The server keeps only a hash; it cannot show this again.',
210
- capabilities: JSON.parse(capsJson),
211
- expires_at: expiresAt,
212
- note: 'Bearer-first grant for safe scopes only. Risk scopes are not delegable until their route has a live-Passkey gate; PoP binding is required before any risk scope or longer-lived delegation (RFC-020).',
431
+ return void res.status(410).json({
432
+ error: 'USE_PAIRING_FLOW', error_code: 'USE_PAIRING_FLOW',
433
+ note: 'Direct grant issuance is disabled. Start pairing with webaz_pair (action=start); the human approves at /#pair with a Passkey, then the agent retrieves the credential with its PKCE verifier. No endpoint mints a bearer without a live Passkey ceremony bound to the pairing.',
213
434
  });
214
435
  });
215
436
  // ── Read: the human's connected agents (no secrets) + recent-use from the audit log (PR-D). ──
@@ -141,6 +141,8 @@ export function registerAnalyticsRoutes(app, deps) {
141
141
  SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
142
142
  SUM(CASE WHEN status IN ('paid','accepted','shipped','picked_up','in_transit','delivered','confirmed') THEN 1 ELSE 0 END) as in_progress_orders,
143
143
  COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv,
144
+ COALESCE(SUM(CASE WHEN status = 'completed' AND COALESCE(payment_rail,'escrow') = 'escrow' THEN total_amount ELSE 0 END), 0) as gmv_escrow,
145
+ COALESCE(SUM(CASE WHEN status = 'completed' AND payment_rail = 'direct_p2p' THEN total_amount ELSE 0 END), 0) as gmv_direct_pay,
144
146
  COALESCE(AVG(CASE WHEN status = 'completed' THEN total_amount END), 0) as aov
145
147
  FROM orders WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
146
148
  `, [user.id, windowDays]));
@@ -1,8 +1,42 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { grantArbitrator, grantArbitratorTx, suspendArbitrator, reinstateArbitrator, revokeArbitrator, listArbitrators } from '../arbitrator-lifecycle.js'; // PR-B/PR-C.2 生产仲裁员生命周期域逻辑
2
3
  export function registerArbitratorRoutes(app, deps) {
3
4
  // 只读/单写站点走 RFC-016 异步 seam;db 保留:apply/withdraw/approve/reject 是
4
5
  // stake 资金路径,状态翻转 + 钱包扣/退必须原子(db.transaction + CAS),Phase 3 迁 pg 行锁。
5
- const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
6
+ const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, consumeGateToken, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
7
+ // ── PR-B:生产仲裁员生命周期(grant/suspend/reinstate/revoke)。门(四端点共用):ROOT/admin auth +
8
+ // 现场真人 Passkey(purpose 绑动作 + purpose_data.user_id 绑目标,杜绝跨动作/跨目标复用)+ admin_audit_log(含失败留痕)。
9
+ // 域逻辑(active-only 授权源 / revoked 终态 / 拒非人类)在 arbitrator-lifecycle.ts;此处只做 auth + Passkey + 审计。
10
+ const arbLifecycle = (action, purpose, fn) => (req, res) => {
11
+ const admin = requireArbitrationAdmin(req, res);
12
+ if (!admin)
13
+ return;
14
+ const userId = String((action === 'grant' ? req.body?.user_id : req.params.user_id) || '');
15
+ const note = (req.body?.note ?? null);
16
+ if (!userId)
17
+ return void errorRes(res, 400, 'MISSING_USER_ID', '缺少目标 user_id');
18
+ const gate = consumeGateToken(admin.id, req.body?.webauthn_token, purpose, (d) => d?.user_id === userId);
19
+ if (!gate.ok) {
20
+ logAdminAction(admin.id, `arbitrator.${action}`, 'user', userId, { ok: false, gate: gate.reason }); // ROOT 尝试也留痕(purpose 不符/无 token)
21
+ return void errorRes(res, 412, 'HUMAN_PRESENCE_REQUIRED', gate.reason || '此操作需现场真人 Passkey 确认');
22
+ }
23
+ const r = fn(userId, admin.id, note);
24
+ logAdminAction(admin.id, `arbitrator.${action}`, 'user', userId, { ok: r.ok, error_code: r.error_code, note });
25
+ if (!r.ok)
26
+ return void errorRes(res, 409, r.error_code || 'ARB_MUTATION_FAILED', r.error || '操作失败');
27
+ res.json({ success: true, user_id: userId, action });
28
+ };
29
+ app.post('/api/admin/arbitrators/grant', arbLifecycle('grant', 'arbitrator_grant', (userId, adminId, note) => grantArbitrator(db, { userId, grantedBy: adminId, note })));
30
+ app.post('/api/admin/arbitrators/:user_id/suspend', arbLifecycle('suspend', 'arbitrator_suspend', (userId, _a, note) => suspendArbitrator(db, { userId, note })));
31
+ app.post('/api/admin/arbitrators/:user_id/reinstate', arbLifecycle('reinstate', 'arbitrator_reinstate', (userId, _a, note) => reinstateArbitrator(db, { userId, note })));
32
+ app.post('/api/admin/arbitrators/:user_id/revoke', arbLifecycle('revoke', 'arbitrator_revoke', (userId, _a, note) => revokeArbitrator(db, { userId, note })));
33
+ // 名册(admin 只读,无需 Passkey)。含 active/suspended/revoked 全量 + 状态。
34
+ app.get('/api/admin/arbitrators', (req, res) => {
35
+ const admin = requireArbitrationAdmin(req, res);
36
+ if (!admin)
37
+ return;
38
+ res.json({ arbitrators: listArbitrators(db) });
39
+ });
6
40
  app.get('/api/arbitrator/eligibility', (req, res) => {
7
41
  const user = auth(req, res);
8
42
  if (!user)
@@ -123,30 +157,49 @@ export function registerArbitratorRoutes(app, deps) {
123
157
  const admin = requireArbitrationAdmin(req, res);
124
158
  if (!admin)
125
159
  return;
126
- const { note } = req.body;
160
+ const note = (req.body?.note ?? null);
127
161
  const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
128
162
  if (!appRow)
129
163
  return void res.json({ error: '申请不存在' });
130
164
  if (appRow.status !== 'pending')
131
165
  return void res.json({ error: '该申请不在待审状态' });
132
- // 原子段:CAS 翻转 pending→approved + 入白名单仅在本请求真翻转时(防并发双批准)
166
+ // PR-B:批准 = 现场真人 Passkey(purpose 绑目标 user_id)+ 走【统一】grantArbitrator(含 revoked-terminal +
167
+ // 拒 system/agent/无 Passkey)。绝不 INSERT OR REPLACE 白名单(那会复活被撤销用户)。审计成功与失败。
168
+ const gate = consumeGateToken(admin.id, req.body?.webauthn_token, 'arbitrator_grant', (d) => d?.user_id === appRow.user_id);
169
+ if (!gate.ok) {
170
+ logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, gate: gate.reason });
171
+ return void errorRes(res, 412, 'HUMAN_PRESENCE_REQUIRED', gate.reason || '此操作需现场真人 Passkey 确认');
172
+ }
173
+ // PR-C.2 原子:grant 白名单 + 翻转申请状态在【同一事务】。任一失败一起回滚 —— 杜绝"grant 成功但申请已被并发撤回/拒绝"。
174
+ let gErr = null;
175
+ let raced = false;
133
176
  try {
134
177
  db.transaction(() => {
135
- const cas = db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
136
- .run(admin.id, note || null, appRow.id);
137
- if (cas.changes === 0)
138
- throw new Error('ARB_RACE');
139
- db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
140
- .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
178
+ const g = grantArbitratorTx(db, { userId: appRow.user_id, grantedBy: admin.id, note: note ?? '外部申请批准' });
179
+ if (!g.ok) {
180
+ gErr = g.error_code || 'GRANT_FAILED';
181
+ throw new Error('ARB_ABORT');
182
+ } // 回滚(此时未写 CAS)
183
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'").run(admin.id, note, appRow.id);
184
+ if (cas.changes === 0) {
185
+ raced = true;
186
+ throw new Error('ARB_ABORT');
187
+ } // 并发已改状态 → 回滚 grant
141
188
  })();
142
189
  }
143
190
  catch (e) {
144
- if (e.message === 'ARB_RACE')
145
- return void res.json({ error: '该申请不在待审状态' });
146
- console.error('[arbitrator approve tx]', e.message);
147
- return void res.status(500).json({ error: '批准失败,请重试' });
191
+ if (e.message !== 'ARB_ABORT')
192
+ throw e;
193
+ }
194
+ if (raced) {
195
+ logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, raced: true });
196
+ return void res.json({ error: '该申请不在待审状态' });
197
+ }
198
+ if (gErr) {
199
+ logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, error_code: gErr });
200
+ return void errorRes(res, 409, gErr, '批准失败');
148
201
  }
149
- logAdminAction(admin.id, 'approve_arbitrator', 'user', appRow.user_id, { note });
202
+ logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: true });
150
203
  res.json({ success: true });
151
204
  });
152
205
  app.post('/api/admin/arbitrator-applications/:id/reject', async (req, res) => {