@seasonkoh/webaz 0.1.31 → 0.1.32

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 (31) hide show
  1. package/README.md +3 -1
  2. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -1
  3. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +4 -0
  4. package/dist/layer1-agent/L1-1-mcp-server/server.js +133 -2
  5. package/dist/layer2-business/L2-6-notifications/notification-engine.js +96 -0
  6. package/dist/layer3-trust/L3-1-dispute-engine/decline-contest-resolve.js +110 -0
  7. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +109 -0
  8. package/dist/pwa/agent-order-minimal-view.js +29 -0
  9. package/dist/pwa/order-action-exec.js +158 -0
  10. package/dist/pwa/order-action-request.js +57 -0
  11. package/dist/pwa/public/app-admin-disputes.js +1 -1
  12. package/dist/pwa/public/app-agent-approvals-order.js +18 -0
  13. package/dist/pwa/public/app-agent-approvals.js +6 -7
  14. package/dist/pwa/public/app-agent-pair.js +2 -2
  15. package/dist/pwa/public/app-arbitrator-admin.js +1 -1
  16. package/dist/pwa/public/app-decline-contest-ruling.js +53 -0
  17. package/dist/pwa/public/app-decline-contest-ui.js +44 -0
  18. package/dist/pwa/public/app-grant-duration.js +20 -0
  19. package/dist/pwa/public/app.js +9 -9
  20. package/dist/pwa/public/i18n.js +24 -1
  21. package/dist/pwa/public/index.html +4 -0
  22. package/dist/pwa/public/openapi.json +68 -23
  23. package/dist/pwa/routes/admin-reports.js +40 -3
  24. package/dist/pwa/routes/agent-grants.js +117 -12
  25. package/dist/pwa/routes/disputes-read.js +17 -1
  26. package/dist/pwa/routes/disputes-write.js +42 -73
  27. package/dist/pwa/routes/orders-action.js +46 -3
  28. package/dist/pwa/server.js +3 -3
  29. package/dist/runtime/agent-grant-scopes.js +26 -8
  30. package/dist/runtime/webaz-schema-helpers.js +28 -0
  31. package/package.json +13 -2
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "WebAZ Protocol API",
5
5
  "version": "0.4.14",
6
- "description": "Auto-generated endpoint inventory (777 endpoints, 17 with full schema). See docs/ for design context."
6
+ "description": "Auto-generated endpoint inventory (780 endpoints, 17 with full schema). See docs/ for design context."
7
7
  },
8
8
  "servers": [
9
9
  {
@@ -648,28 +648,6 @@
648
648
  ]
649
649
  }
650
650
  },
651
- "/api/admin/decline-contests": {
652
- "get": {
653
- "summary": "仲裁员待办:列出所有被举证的临时判责拒单",
654
- "tags": [],
655
- "security": [
656
- {
657
- "bearerAuth": []
658
- }
659
- ]
660
- }
661
- },
662
- "/api/admin/decline-contests/:orderId/resolve": {
663
- "post": {
664
- "summary": "仲裁员裁决",
665
- "tags": [],
666
- "security": [
667
- {
668
- "bearerAuth": []
669
- }
670
- ]
671
- }
672
- },
673
651
  "/api/admin/direct-receive/aml-flags": {
674
652
  "post": {
675
653
  "summary": "POST /api/admin/direct-receive/aml-flags",
@@ -1035,6 +1013,20 @@
1035
1013
  ]
1036
1014
  }
1037
1015
  },
1016
+ "/api/admin/disputes/:id/decline-contest-resolve": {
1017
+ "post": {
1018
+ "summary": "(dispute CAS + COI + 终态 completed + 结算 + 审计,单事务全回滚)。admin override 不占用 assigned_arbitrators。",
1019
+ "tags": [
1020
+ "admin",
1021
+ "dispute"
1022
+ ],
1023
+ "security": [
1024
+ {
1025
+ "bearerAuth": []
1026
+ }
1027
+ ]
1028
+ }
1029
+ },
1038
1030
  "/api/admin/economic-summary": {
1039
1031
  "get": {
1040
1032
  "summary": "隐私第一:运营财务,仅 protocol admin 可见。",
@@ -2764,6 +2756,48 @@
2764
2756
  "security": []
2765
2757
  }
2766
2758
  },
2759
+ "/api/agent/orders": {
2760
+ "get": {
2761
+ "summary": "买家地址/联系/gift_recipient 连取都不取(I6)。纯只读,无任何执行(order_action_request 在 PR2/PR3 才有提交/执行)。",
2762
+ "tags": [
2763
+ "order"
2764
+ ],
2765
+ "security": [
2766
+ {
2767
+ "grantBearer": []
2768
+ }
2769
+ ],
2770
+ "x-webaz-grant-scope": "seller_orders_read_minimal"
2771
+ }
2772
+ },
2773
+ "/api/agent/orders/:id": {
2774
+ "get": {
2775
+ "summary": "GET /api/agent/orders/:id",
2776
+ "tags": [
2777
+ "order"
2778
+ ],
2779
+ "security": [
2780
+ {
2781
+ "grantBearer": []
2782
+ }
2783
+ ],
2784
+ "x-webaz-grant-scope": "seller_orders_read_minimal"
2785
+ }
2786
+ },
2787
+ "/api/agent/orders/:orderId/action-request": {
2788
+ "post": {
2789
+ "summary": "同 (order_id,action) 双 pending 被唯一索引拒。执行(accept/ship)在 PR3 经人 Passkey 批准后由服务端跑。",
2790
+ "tags": [
2791
+ "order"
2792
+ ],
2793
+ "security": [
2794
+ {
2795
+ "grantBearer": []
2796
+ }
2797
+ ],
2798
+ "x-webaz-grant-scope": "order_action_request"
2799
+ }
2800
+ },
2767
2801
  "/api/agent/seller/products": {
2768
2802
  "get": {
2769
2803
  "summary": "The consumption (allow AND the permission_required deny) is audited by the middleware.",
@@ -3027,6 +3061,17 @@
3027
3061
  ]
3028
3062
  }
3029
3063
  },
3064
+ "/api/arbitrator/pending-count": {
3065
+ "get": {
3066
+ "summary": "非仲裁员返回 0(不报错,前端 badge 拉取对所有人无害)。",
3067
+ "tags": [],
3068
+ "security": [
3069
+ {
3070
+ "bearerAuth": []
3071
+ }
3072
+ ]
3073
+ }
3074
+ },
3030
3075
  "/api/arbitrator/status": {
3031
3076
  "get": {
3032
3077
  "summary": "GET /api/arbitrator/status",
@@ -1,7 +1,9 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { resolveDeclineContestDispute } from '../../layer3-trust/L3-1-dispute-engine/decline-contest-resolve.js'; // PR3 唯一裁决器(admin fallback 入口)
3
+ import { notifyDeclineContestResolved } from '../../layer2-business/L2-6-notifications/notification-engine.js';
2
4
  export function registerAdminReportsRoutes(app, deps) {
3
- // db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
- const { requireContentAdmin, requireArbitrationAdmin, requireProtocolAdmin } = deps;
5
+ // 读表盘走 RFC-016 异步 seam(dbOne/dbAll);唯一例外 = decline-contest 裁决器需 sync db.transaction(domain 层),用 deps.db
6
+ const { db, requireContentAdmin, requireArbitrationAdmin, requireProtocolAdmin, requireHumanPresence } = deps;
5
7
  app.get('/api/admin/orders', async (req, res) => {
6
8
  const admin = requireContentAdmin(req, res);
7
9
  if (!admin)
@@ -42,6 +44,7 @@ export function registerAdminReportsRoutes(app, deps) {
42
44
  const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
43
45
  const where = [];
44
46
  const params = [];
47
+ // PR2:decline_contest 现进入 admin 监督台(前端按 dispute_type 打"拒单举证仲裁"标签)。
45
48
  if (status) {
46
49
  where.push('d.status = ?');
47
50
  params.push(status);
@@ -51,7 +54,7 @@ export function registerAdminReportsRoutes(app, deps) {
51
54
  params.push(rail);
52
55
  }
53
56
  const rows = await dbAll(`
54
- SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status,
57
+ SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status, d.dispute_type,
55
58
  d.created_at, d.respond_deadline, d.arbitrate_deadline, d.resolved_at,
56
59
  d.verdict, d.ruling_type, d.assigned_arbitrators,
57
60
  u1.name as initiator_name, u2.name as defendant_name,
@@ -68,6 +71,40 @@ export function registerAdminReportsRoutes(app, deps) {
68
71
  const counts = await dbAll('SELECT status, COUNT(*) n FROM disputes GROUP BY status');
69
72
  res.json({ disputes: rows, counts: Object.fromEntries(counts.map(c => [c.status, c.n])), total: counts.reduce((s, c) => s + c.n, 0) });
70
73
  });
74
+ // PR3 admin fallback:仲裁窗口(arbitrate_deadline)过后,仲裁 admin 可 override 裁决卡住的拒单举证仲裁。
75
+ // §3 仲裁员优先(窗口未过 → resolver 抛 FALLBACK_TOO_EARLY);走【与仲裁员完全相同】的唯一 resolver
76
+ // (dispute CAS + COI + 终态 completed + 结算 + 审计,单事务全回滚)。admin override 不占用 assigned_arbitrators。
77
+ app.post('/api/admin/disputes/:id/decline-contest-resolve', async (req, res) => {
78
+ const admin = requireArbitrationAdmin(req, res);
79
+ if (!admin)
80
+ return;
81
+ const { decision, reason } = (req.body ?? {});
82
+ if (decision !== 'decline_no_fault_upheld' && decision !== 'decline_fault_confirmed')
83
+ return void res.status(400).json({ error: "decision 必须为 'decline_no_fault_upheld' / 'decline_fault_confirmed'", error_code: 'BAD_DECISION' });
84
+ if (!reason || !String(reason).trim())
85
+ return void res.status(400).json({ error: '请提供裁决理由', error_code: 'REASON_REQUIRED' });
86
+ // Passkey purpose_data 绑 dispute_id + action + decision —— 钱路/状态闭环动作,fail-closed:
87
+ // token 必须【显式绑定】本案 + 本动作 + 本结果。绝不允许 null(未绑定 token)或缺字段通过,否则一个泛化的
88
+ // arbitrate token 就能对任意过期 decline-contest 触发退款/罚没/completed。
89
+ const hp = requireHumanPresence(admin.id, 'arbitrate', req.body?.webauthn_token, 'require_human_presence_for_arbitrate', (data) => {
90
+ const d = data;
91
+ return d != null && typeof d === 'object' && d.dispute_id === req.params.id && d.action === 'decline_contest_resolve' && d.decision === decision;
92
+ });
93
+ if (!hp.ok)
94
+ return void res.status(412).json({ error: hp.reason || '此操作需真实人工 WebAuthn 验证', error_code: hp.error_code || 'HUMAN_PRESENCE_REQUIRED' });
95
+ try {
96
+ const r = resolveDeclineContestDispute(db, req.params.id, admin.id, decision, reason, 'admin_fallback');
97
+ try {
98
+ notifyDeclineContestResolved(db, r.orderId, r.decision);
99
+ }
100
+ catch { /* 通知失败不影响已完成结算 */ }
101
+ return void res.json({ success: true, outcome: r.decision, order_status: 'completed', source: 'admin_fallback' });
102
+ }
103
+ catch (e) {
104
+ const err = e;
105
+ return void res.status(err.http || 400).json({ error: err.message || '裁决失败', error_code: err.code || 'DC_RESOLVE_FAILED' });
106
+ }
107
+ });
71
108
  app.get('/api/admin/verify-tasks', async (req, res) => {
72
109
  const admin = requireArbitrationAdmin(req, res);
73
110
  if (!admin)
@@ -1,9 +1,13 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
3
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';
4
+ import { validateRequestedCapabilities, grantIsActive, resolveBundle, durationAllowedForScopes, suggestedDurationForScopes, allowedDurationsForScopes, 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
+ import { minimalSellerOrderView, MINIMAL_ORDER_COLUMNS } from '../agent-order-minimal-view.js'; // RFC-021 §6a 最小化订单读投影
8
+ import { createOrderActionRequest } from '../order-action-request.js'; // RFC-021 PR2 order-action 请求 domain(sync tx 在 domain 层,不增 route seam)
9
+ import { approveAndExecuteOrderAction } from '../order-action-exec.js'; // RFC-021 PR3 approve→执行(CAS approved + 执行 + executed_at CAS,domain 层)
10
+ import { notifyTransition } from '../../layer2-business/L2-6-notifications/notification-engine.js'; // 执行后通知买卖双方
7
11
  // Bounds on a pairing request (anti-bloat for the anonymous start endpoint).
8
12
  const MAX_CAPABILITIES = 12;
9
13
  const MAX_CONSTRAINTS_JSON = 2000;
@@ -28,7 +32,10 @@ function consentView(p) {
28
32
  capabilities: safeParseCaps(p.capabilities), // server-validated safe scopes
29
33
  status: p.status,
30
34
  expires_at: p.expires_at,
31
- notice: 'Approving issues a scoped, short-lived, revocable delegation grant NOT your api_key, NOT your funds. Safe (read/draft) scopes only; it can never move money, vote, arbitrate, or change keys.',
35
+ // Duration-choice: agent's suggested lifetime + the durations the human may pick (safe scopes up to 30d).
36
+ suggested_duration: p.grant_duration || suggestedDurationForScopes(safeParseCaps(p.capabilities).map(c => String(c?.capability || '')).filter(Boolean)),
37
+ allowed_durations: allowedDurationsForScopes(safeParseCaps(p.capabilities).map(c => String(c?.capability || '')).filter(Boolean)),
38
+ notice: 'Approving issues a scoped, revocable delegation grant — NOT your api_key, NOT your funds. Safe (read/draft) scopes only; it can never move money, vote, arbitrate, or change keys. You choose how long it lasts.',
32
39
  };
33
40
  }
34
41
  export function registerAgentGrantsRoutes(app, deps) {
@@ -117,6 +124,33 @@ export function registerAgentGrantsRoutes(app, deps) {
117
124
  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]);
118
125
  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.' });
119
126
  });
127
+ // RFC-021 §6a — 最小化订单读(safe scope seller_orders_read_minimal)。仅读该 agent 之人(卖家)的订单;
128
+ // ALLOWLIST 投影(minimalSellerOrderView)只产出 6 字段,SELECT 只取非 PII 列(MINIMAL_ORDER_COLUMNS) ——
129
+ // 买家地址/联系/gift_recipient 连取都不取(I6)。纯只读,无任何执行(order_action_request 在 PR2/PR3 才有提交/执行)。
130
+ app.get('/api/agent/orders', requireAgentGrantScope('seller_orders_read_minimal'), async (req, res) => {
131
+ const p = req.agentGrant;
132
+ const rows = await dbAll(`SELECT ${MINIMAL_ORDER_COLUMNS.join(', ')} FROM orders WHERE seller_id = ? ORDER BY created_at DESC LIMIT 200`, [p.human_id]);
133
+ res.json({ seller_id: p.human_id, agent_label: p.agent_label, count: rows.length, orders: rows.map(o => minimalSellerOrderView(o, db)), note: 'RFC-021 minimal order read (safe scope seller_orders_read_minimal). No buyer address/contact; no execution.' });
134
+ });
135
+ app.get('/api/agent/orders/:id', requireAgentGrantScope('seller_orders_read_minimal'), async (req, res) => {
136
+ const p = req.agentGrant;
137
+ const o = await dbOne(`SELECT ${MINIMAL_ORDER_COLUMNS.join(', ')} FROM orders WHERE id = ? AND seller_id = ?`, [req.params.id, p.human_id]);
138
+ if (!o)
139
+ return void res.status(404).json({ error: '订单不存在或不属于你', error_code: 'ORDER_NOT_FOUND' });
140
+ res.json({ order: minimalSellerOrderView(o, db) });
141
+ });
142
+ // RFC-021 PR2 — order-action 请求提交(safe scope order_action_request)。SUBMIT-only:写 pending,【绝不执行】。
143
+ // D2 拒 decline;归属校验 seller 本人;ship 须带 tracking+evidence_ref(I4 提交侧);地址永不入参/入 audit(I6);
144
+ // 同 (order_id,action) 双 pending 被唯一索引拒。执行(accept/ship)在 PR3 经人 Passkey 批准后由服务端跑。
145
+ app.post('/api/agent/orders/:orderId/action-request', requireAgentGrantScope('order_action_request'), async (req, res) => {
146
+ const p = req.agentGrant;
147
+ const b = (req.body ?? {});
148
+ const orderId = String(req.params.orderId);
149
+ const r = createOrderActionRequest(db, { orderId, action: String(b.action ?? ''), rawParams: b.action_params, grantId: p.grant_id, humanId: p.human_id, agentLabel: p.agent_label, generateId });
150
+ if (!r.ok)
151
+ return void res.status(r.http || 400).json({ error: r.error, error_code: r.error_code });
152
+ res.json({ success: true, request_id: r.request_id, order_id: orderId, action: b.action, params_hash: r.params_hash, approval_url: '/#agent-approvals', note: 'Pending human Passkey approval. NOT executed — execution (accept/ship) lands in RFC-021 PR3.' });
153
+ });
120
154
  // POST create a DRAFT product via a delegation grant (Catalog Agent, safe scope seller_product_draft). The
121
155
  // draft is FORCED to status='warehouse' (not public/sellable). PUBLISHING STAYS HUMAN-ONLY — the human
122
156
  // flips warehouse→active in the existing 我的商品→仓库 UI (publish is never delegated to a grant, matching
@@ -129,6 +163,19 @@ export function registerAgentGrantsRoutes(app, deps) {
129
163
  const human = await dbOne('SELECT id, role FROM users WHERE id = ?', [p.human_id]);
130
164
  if (!human || human.role !== 'seller')
131
165
  return void res.status(403).json({ error: 'the grant owner is not a seller — product drafts need a seller account', error_code: 'NOT_A_SELLER' });
166
+ // Accurate error codes for the GRANT path WITHOUT touching the shared handler / human path: the create
167
+ // handler emits legacy validation errors as `200 + {error}` (no success). For a grant caller that's
168
+ // misleading, so translate exactly those to `400 VALIDATION_ERROR`. Explicit res.status(400/429/500…)
169
+ // and the success ({product_id}) response pass through unchanged.
170
+ const origJson = res.json.bind(res);
171
+ res.json = (body) => {
172
+ const b = body;
173
+ if (res.statusCode === 200 && b && typeof b === 'object' && b.error && !b.success) {
174
+ res.status(400);
175
+ return origJson({ error: b.error, error_code: 'VALIDATION_ERROR' });
176
+ }
177
+ return origJson(body);
178
+ };
132
179
  await createProductDraftHandler(req, res, human, {
133
180
  forceStatus: 'warehouse',
134
181
  skipExternalLinkEffects: true, // a SAFE draft grant must never trigger wallet debit / verify_tasks / auto-verified links (source_url stays inert metadata)
@@ -211,8 +258,21 @@ export function registerAgentGrantsRoutes(app, deps) {
211
258
  const user = auth(req, res);
212
259
  if (!user)
213
260
  return;
214
- 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()]);
215
- res.json({ requests: rows.map(r => ({ ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) })) });
261
+ const rows = await dbAll("SELECT id, agent_label, requested_scopes, permission_bundle, reason, task_context, risk_level, duration, created_at, expires_at, kind, order_id, order_action, params_hash, action_params 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()]);
262
+ // order_action:额外返回 kind/order_id/order_action/params_hash/action_params(action_params PR2 sanitize,
263
+ // 只含 tracking/evidence_ref,【无地址/PII】)。前端据此绑 Passkey purpose_data {request_id, order_id, action, params_hash}。
264
+ res.json({ requests: rows.map(r => {
265
+ const base = { ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) };
266
+ if (r.kind === 'order_action') {
267
+ try {
268
+ base.action_params = r.action_params ? JSON.parse(String(r.action_params)) : {};
269
+ }
270
+ catch {
271
+ base.action_params = {};
272
+ }
273
+ }
274
+ return base;
275
+ }) });
216
276
  });
217
277
  // GET list the requests THIS grant created — GRANT-authed (the agent, via webaz_pair), so an agent can poll
218
278
  // its own request status (pending/approved/rejected/expired) without hitting the target surface. Bound to
@@ -238,11 +298,34 @@ export function registerAgentGrantsRoutes(app, deps) {
238
298
  if (!user)
239
299
  return;
240
300
  const now = new Date().toISOString();
241
- 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]);
301
+ const r = await dbOne('SELECT human_id, grant_id, requested_scopes, permission_bundle, duration, status, expires_at, kind, order_id, order_action, params_hash FROM agent_permission_requests WHERE id = ?', [req.params.id]);
242
302
  if (!r)
243
303
  return void res.status(404).json({ error: 'permission_request_not_found' });
244
304
  if (r.human_id !== user.id)
245
305
  return void res.status(403).json({ error: 'not your permission request' });
306
+ // RFC-021 PR2:order-action 请求分流。Passkey 绑三元组 (order_id, action, params_hash)(对齐 admin fallback 严格绑定);
307
+ // 过期/pending 判定【原子在 domain 的 CAS 里】(P1-b,含 expires_at > now),不做两步式预检查。
308
+ // 批到 approved 就【停】—— 绝不执行、不改订单、不碰钱路(execute 在 PR3)。
309
+ if ((r.kind ?? 'scope_grant') === 'order_action') {
310
+ const hp = requireHumanPresence(user.id, 'agent_permission_approve', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_permission_approve', (data) => { const d = data; return d != null && typeof d === 'object' && d.request_id === req.params.id && d.order_id === r.order_id && d.action === r.order_action && d.params_hash === r.params_hash; });
311
+ if (!hp.ok)
312
+ return void res.status(412).json({ error: hp.reason, error_code: hp.error_code });
313
+ // PR3:CAS→approved 后【由服务端在 seller 本人授权下执行】accept/ship(strictTracking=true,守卫全在执行器内)。
314
+ // 执行成功写 executed_at(I5 幂等);执行失败请求保持 approved 可重试。执行器对 agent-bearer 不可达(/approve 需人类 session)。
315
+ const ar = approveAndExecuteOrderAction(db, req.params.id, user.id, r.grant_id, now, generateId);
316
+ if (!ar.ok)
317
+ return void res.status(ar.http || 409).json({ error: ar.error, error_code: ar.error_code });
318
+ // 执行成功后通知买卖双方(事务外;通知失败不回滚已完成的状态跃迁)
319
+ if (!ar.already_executed && ar.order_status) {
320
+ try {
321
+ const fromS = ar.order_status === 'accepted' ? 'paid' : 'accepted';
322
+ notifyTransition(db, r.order_id, fromS, ar.order_status);
323
+ }
324
+ catch { /* */ }
325
+ }
326
+ return void res.json({ success: true, kind: 'order_action', status: 'executed', order_id: r.order_id, action: r.order_action, order_status: ar.order_status, already_executed: ar.already_executed || false });
327
+ }
328
+ // scope_grant:保留既有两步(下方还有 grant-active 复检 + 原子 tx;非 order-action)。
246
329
  if (r.status !== 'pending' || r.expires_at <= now)
247
330
  return void res.status(409).json({ error: 'permission_request_not_pending', status: r.status });
248
331
  // Live Passkey, bound to THIS request (a token minted for request A can't approve B).
@@ -264,9 +347,17 @@ export function registerAgentGrantsRoutes(app, deps) {
264
347
  const grant = await dbOne('SELECT grant_id, capabilities, status, expires_at, revoked_at FROM agent_delegation_grants WHERE grant_id = ?', [r.grant_id]);
265
348
  if (!grant || !grantIsActive(grant, now))
266
349
  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)' });
267
- // Union new scopes into the grant; set bundle; extend expiry (never shorten). 'once' short 1h window.
350
+ // Duration: the HUMAN's submitted value is authoritative. If body.duration is PRESENT but not allowed →
351
+ // 400 (no silent fallback; nothing claimed/expanded below). Only when ABSENT do we fall back to the
352
+ // agent's requested duration. Checked BEFORE the tx so an invalid value never claims/extends anything.
353
+ const bodyDur = (req.body || {}).duration;
354
+ if (bodyDur !== undefined && bodyDur !== null && !durationAllowedForScopes(reqScopes, bodyDur)) {
355
+ return void res.status(400).json({ error: 'invalid grant duration', error_code: 'INVALID_GRANT_DURATION', allowed_durations: allowedDurationsForScopes(reqScopes) });
356
+ }
357
+ // Union new scopes into the grant; set bundle; extend expiry (never shorten).
268
358
  const union = [...new Set([...scopeNames(grant.capabilities), ...reqScopes])].map(s => ({ capability: s, constraints: {} }));
269
- const secs = durationToSeconds(r.duration) || 3600;
359
+ const effDuration = (bodyDur !== undefined && bodyDur !== null) ? bodyDur : r.duration;
360
+ const secs = durationToSeconds(effDuration) || 3600;
270
361
  const newExpiry = new Date(Date.now() + secs * 1000).toISOString();
271
362
  const expiresAt = newExpiry > grant.expires_at ? newExpiry : grant.expires_at;
272
363
  // ATOMIC (RFC-020 invariant: a grant expansion is audited-or-it-does-not-happen; parity with arbitrator
@@ -294,7 +385,7 @@ export function registerAgentGrantsRoutes(app, deps) {
294
385
  }
295
386
  if (outcome === 'not_pending')
296
387
  return void res.status(409).json({ error: 'permission_request_not_pending' });
297
- res.json({ success: true, grant_id: grant.grant_id, scopes: union.map(u => u.capability), permission_bundle: r.permission_bundle, expires_at: expiresAt });
388
+ res.json({ success: true, grant_id: grant.grant_id, scopes: union.map(u => u.capability), permission_bundle: r.permission_bundle, duration: effDuration, expires_at: expiresAt });
298
389
  });
299
390
  // POST reject — human-authed. Terminal 'rejected'; nothing is granted.
300
391
  app.post('/api/agent-grants/permission-requests/:id/reject', async (req, res) => {
@@ -341,13 +432,17 @@ export function registerAgentGrantsRoutes(app, deps) {
341
432
  const capsJson = JSON.stringify(v.safe.map(c => ({ capability: c, constraints: (caps.find(x => x?.capability === c)?.constraints) || {} })));
342
433
  if (capsJson.length > MAX_CONSTRAINTS_JSON)
343
434
  return void res.status(400).json({ error: 'capabilities_too_large', max_bytes: MAX_CONSTRAINTS_JSON });
344
- await dbRun('INSERT INTO agent_pairing_sessions (pairing_id, user_code, code_challenge, agent_label, agent_pubkey, reason, capabilities, status, expires_at) VALUES (?,?,?,?,?,?,?,?,?)', [pairingId, userCode, codeChallenge, label, pubkey, reason, capsJson, 'pending', expiresAt]);
435
+ // Agent SUGGESTS a grant lifetime; the human accepts or overrides it at approve. Safe-scope only up to 30d.
436
+ const reqDuration = durationAllowedForScopes(v.safe, body.duration) ? body.duration : suggestedDurationForScopes(v.safe);
437
+ await dbRun('INSERT INTO agent_pairing_sessions (pairing_id, user_code, code_challenge, agent_label, agent_pubkey, reason, capabilities, grant_duration, status, expires_at) VALUES (?,?,?,?,?,?,?,?,?,?)', [pairingId, userCode, codeChallenge, label, pubkey, reason, capsJson, reqDuration, 'pending', expiresAt]);
345
438
  res.status(201).json({
346
439
  pairing_id: pairingId,
347
440
  user_code: userCode,
348
441
  approve_url: `/#pair?code=${userCode}`,
349
442
  expires_at: expiresAt,
350
- note: 'Ask the human to open approve_url at webaz.xyz (logged in) and approve. Then retrieve the credential with the PKCE verifier.',
443
+ suggested_duration: reqDuration,
444
+ allowed_durations: allowedDurationsForScopes(v.safe),
445
+ note: 'Ask the human to open approve_url at webaz.xyz (logged in) and approve. The human picks the grant duration (your suggested_duration is pre-selected; they can change it). Then retrieve the credential with the PKCE verifier.',
351
446
  });
352
447
  });
353
448
  // (pair 2) Human reviews the server-generated consent — human-authenticated.
@@ -390,7 +485,17 @@ export function registerAgentGrantsRoutes(app, deps) {
390
485
  if (!v.ok)
391
486
  return void res.status(403).json({ error: 'pairing_rejected', rejected: v.rejected });
392
487
  const grantId = generateId('grt');
393
- const expiresAt = new Date(Date.now() + clampTtlSeconds(undefined) * 1000).toISOString();
488
+ // Duration: the HUMAN's submitted value is authoritative. If body.duration is PRESENT but not in the
489
+ // safe-scope matrix → 400 (no silent fallback; nothing claimed/issued below). Only when ABSENT do we
490
+ // fall back to the agent's suggestion (grant_duration) then the safe default.
491
+ const bodyDur = (req.body || {}).duration;
492
+ if (bodyDur !== undefined && bodyDur !== null && !durationAllowedForScopes(v.safe, bodyDur)) {
493
+ return void res.status(400).json({ error: 'invalid grant duration', error_code: 'INVALID_GRANT_DURATION', allowed_durations: allowedDurationsForScopes(v.safe) });
494
+ }
495
+ const chosenDuration = (bodyDur !== undefined && bodyDur !== null)
496
+ ? bodyDur
497
+ : (durationAllowedForScopes(v.safe, p.grant_duration) ? p.grant_duration : suggestedDurationForScopes(v.safe));
498
+ const expiresAt = new Date(Date.now() + (durationToSeconds(chosenDuration) || 3600) * 1000).toISOString();
394
499
  // 先【CAS 抢占】pending 配对(唯一赢家),再插 grant —— 竞态下输家 changes!==1 直接 409、不插任何 grant,
395
500
  // 杜绝"插了 grant 但配对更新失败"留下 token_hash NULL 的 orphan grant(污染连接记录)。
396
501
  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]);
@@ -398,7 +503,7 @@ export function registerAgentGrantsRoutes(app, deps) {
398
503
  return void res.status(409).json({ error: 'pairing_not_pending_or_expired' });
399
504
  // Grant created WITHOUT a token (token_hash NULL) — the bearer is minted only at retrieval.
400
505
  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]);
401
- res.json({ success: true, grant_id: grantId, capabilities: caps });
506
+ res.json({ success: true, grant_id: grantId, capabilities: caps, duration: chosenDuration, expires_at: expiresAt });
402
507
  });
403
508
  // (pair 3b) Human rejects — human-authenticated. Terminal 'rejected' → agent's retrieve fails clearly (no silent lingering).
404
509
  // 拒绝是保护性动作,无需 Passkey(不签发任何凭证)。幂等:仅 pending 可拒。
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { getRecentResolvedDisputes } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js'; // 历史裁决(已结)供仲裁员"已结"tab
2
3
  export function registerDisputesReadRoutes(app, deps) {
3
4
  const { db, auth, errorRes, getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles, isEligibleArbitrator, isArbitrationAdmin } = deps;
4
5
  // 仲裁员:查看所有开放争议
@@ -9,7 +10,22 @@ export function registerDisputesReadRoutes(app, deps) {
9
10
  const elig = isEligibleArbitrator(user.id);
10
11
  if (!elig.ok)
11
12
  return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员访问');
12
- res.json(await getOpenDisputes(db));
13
+ // 待裁(open+in_review)+ 近期已结(resolved/dismissed)—— 前端"待裁/已结"sub-tab 客户端切分;
14
+ // getOpenDisputes 语义不变(MCP 复用),历史走 getRecentResolvedDisputes(LIMIT 50)。
15
+ const open = await getOpenDisputes(db);
16
+ const resolved = await getRecentResolvedDisputes(db, 50);
17
+ res.json([...open, ...resolved]);
18
+ });
19
+ // PR2 统一仲裁台待办角标:仲裁员待处理案数(open+in_review,含并入的 decline_contest)。轻量 count,喂前端 badge。
20
+ // 非仲裁员返回 0(不报错,前端 badge 拉取对所有人无害)。
21
+ app.get('/api/arbitrator/pending-count', async (req, res) => {
22
+ const user = auth(req, res);
23
+ if (!user)
24
+ return;
25
+ if (!isEligibleArbitrator(user.id).ok)
26
+ return void res.json({ count: 0 });
27
+ const row = await dbOne("SELECT COUNT(*) n FROM disputes WHERE status IN ('open','in_review')");
28
+ res.json({ count: row?.n ?? 0 });
13
29
  });
14
30
  // A2 同类判例推荐
15
31
  app.get('/api/disputes/:id/similar-cases', async (req, res) => {
@@ -1,14 +1,14 @@
1
1
  import express from 'express';
2
- // RFC-007 stage 5:客观拒单仲裁翻案直接复用 L0 状态机/结算(纯 db 函数,无副作用)
3
- import { transition, settleFault, settleDeclinedNoFault } from '../../layer0-foundation/L0-2-state-machine/engine.js';
4
2
  // RFC-014 PR5 — 争议后佣金/基金 clawback 走整数 base-units + 绝对值落库。
5
3
  import { toUnits, toDecimal, mulRate } from '../../money.js';
6
4
  import { applyWalletDelta } from '../../ledger.js';
7
5
  // RFC-016 Phase 1 — 纯只读端点/校验读/SNF 分发读/标记写 → async seam;arbitrate 仲裁核心(原子领取 +
8
6
  // 2 settlement db.transaction + reputation/strike/publish)与 tx 内 appendAuditLog 保持同步(Phase 3 迁 pg)。
9
- import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
7
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
10
8
  import { arbitratorHasConflict } from '../arbitrator-lifecycle.js'; // PR-B COI 硬门
11
9
  import { dismissDisputeToNegotiation } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js'; // 仲裁员驳回仲裁,退回协商(非胜负裁决)
10
+ import { resolveDeclineContestDispute } from '../../layer3-trust/L3-1-dispute-engine/decline-contest-resolve.js'; // PR3 拒单举证唯一裁决器
11
+ import { notifyDeclineContestResolved } from '../../layer2-business/L2-6-notifications/notification-engine.js'; // 裁决落定通知买卖双方
12
12
  export function registerDisputesWriteRoutes(app, deps) {
13
13
  const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam, notifyTransition } = deps;
14
14
  // PR-E:案件级仲裁操作(裁定 / 补证)统一的【原子领取或校验指派】。assigned 为空 → CAS 首位领取;
@@ -34,72 +34,10 @@ export function registerDisputesWriteRoutes(app, deps) {
34
34
  }
35
35
  return assigned.includes(userId) ? { ok: true } : { ok: false, error_code: 'NOT_ASSIGNED_ARBITRATOR' };
36
36
  };
37
- // ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
38
- // 卖家 contest_decline 后,订单 = fault_seller + decline_objective_pending=1 + decline_contested=1(未结算)。
39
- // 仲裁员(真实人工 + WebAuthn)裁决:uphold → declined_nofault(免责全退+退质押);reject → 违约结算。
40
- const SYS = 'sys_protocol';
41
- // 仲裁员待办:列出所有被举证的临时判责拒单
42
- app.get('/api/admin/decline-contests', async (req, res) => {
43
- const user = auth(req, res);
44
- if (!user)
45
- return;
46
- const elig = isEligibleArbitrator(user.id);
47
- if (!elig.ok)
48
- return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
49
- const rows = await dbAll(`
50
- SELECT id AS order_id, buyer_id, seller_id, product_id, total_amount, decline_reason_code, declined_at, decline_contest_deadline
51
- FROM orders
52
- WHERE status = 'fault_seller' AND COALESCE(decline_objective_pending,0)=1 AND COALESCE(decline_contested,0)=1 AND settled_fault_at IS NULL
53
- ORDER BY declined_at ASC
54
- `);
55
- res.json({ contests: rows });
56
- });
57
- // 仲裁员裁决
58
- app.post('/api/admin/decline-contests/:orderId/resolve', async (req, res) => {
59
- const user = auth(req, res);
60
- if (!user)
61
- return;
62
- const elig = isEligibleArbitrator(user.id);
63
- if (!elig.ok)
64
- return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
65
- // 铁律:仲裁需真实人工 WebAuthn
66
- const hp = requireHumanPresence(user.id, 'arbitrate', req.body?.webauthn_token, 'require_human_presence_for_arbitrate');
67
- if (!hp.ok)
68
- return void errorRes(res, 412, hp.error_code || 'HUMAN_PRESENCE_REQUIRED', hp.reason || '此操作需真实人工 WebAuthn 验证');
69
- const { decision, reason } = req.body ?? {};
70
- if (!['uphold', 'reject'].includes(decision))
71
- return void errorRes(res, 400, 'BAD_DECISION', "decision 必须为 'uphold'(认定无责) 或 'reject'(驳回,判违约)");
72
- if (!reason || !String(reason).trim())
73
- return void errorRes(res, 400, 'REASON_REQUIRED', '请提供裁决理由');
74
- const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.orderId]);
75
- if (!order)
76
- return void res.status(404).json({ error: '订单不存在' });
77
- if (order.status !== 'fault_seller' || Number(order.decline_objective_pending) !== 1 || Number(order.decline_contested) !== 1 || order.settled_fault_at) {
78
- return void errorRes(res, 400, 'NOT_CONTESTED_PROVISIONAL', '本订单不是【已举证·待裁决】的临时判责拒单');
79
- }
80
- try {
81
- appendOrderEvent(db, { orderId: req.params.orderId, eventType: 'transition', fromStatus: 'fault_seller', toStatus: 'fault_seller', actorId: user.id, actorRole: 'arbitrator', extra: { action: 'decline_contest_ruling', decision, reason } });
82
- }
83
- catch (e) {
84
- console.warn('[order-chain] decline ruling event:', e.message);
85
- }
86
- if (decision === 'uphold') {
87
- // 认定客观无责 → declined_nofault:全退买家 + 退卖家质押,零罚没
88
- const r1 = transition(db, req.params.orderId, 'declined_nofault', user.id, [], `客观拒单仲裁维持(无责):${reason}`);
89
- if (!r1.success)
90
- return void res.json({ error: r1.error });
91
- settleDeclinedNoFault(db, req.params.orderId);
92
- transition(db, req.params.orderId, 'completed', SYS, [], '无责拒单结算完成');
93
- logAdminAction(user.id, 'decline_contest_uphold', 'order', req.params.orderId, { reason });
94
- return void res.json({ success: true, outcome: 'declined_nofault', note: '裁决:客观无责。买家已全额退款,卖家质押已退回,无罚没。' });
95
- }
96
- // reject → 违约结算(终结临时判责)
97
- settleFault(db, req.params.orderId, 'fault_seller');
98
- transition(db, req.params.orderId, 'completed', SYS, [], `客观拒单仲裁驳回 → 违约结算:${reason}`);
99
- db.prepare('UPDATE orders SET decline_objective_pending = 0, decline_contested = 0 WHERE id = ?').run(req.params.orderId);
100
- logAdminAction(user.id, 'decline_contest_reject', 'order', req.params.orderId, { reason });
101
- res.json({ success: true, outcome: 'fault_seller', note: '裁决:驳回。按违约结算,买家已全额退款,卖家质押按规则罚没。' });
102
- });
37
+ // RFC-007 客观拒单举证仲裁已并入统一 disputes 仲裁台(PR1-3,#279-#281):dispute_type='decline_contest'。
38
+ // 旧订单级 GET /api/admin/decline-contests 列表 + POST .../:orderId/resolve 裁决端点【已物理移除】(PR4)。
39
+ // 现统一走:仲裁员 POST /api/disputes/:id/arbitrate(ruling=decline_no_fault_upheld|decline_fault_confirmed);
40
+ // admin 兜底 POST /api/admin/disputes/:id/decline-contest-resolve;两者都经唯一 resolveDeclineContestDispute。
103
41
  // 被诉方反驳
104
42
  app.post('/api/disputes/:id/respond', async (req, res) => {
105
43
  const user = auth(req, res);
@@ -132,9 +70,15 @@ export function registerDisputesWriteRoutes(app, deps) {
132
70
  const elig = isEligibleArbitrator(user.id);
133
71
  if (!elig.ok)
134
72
  return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
135
- // 2026-05-23 Agent 治理铁律:仲裁需真实人工
73
+ // 2026-05-23 Agent 治理铁律:仲裁需真实人工。
74
+ // decline_contest 两选裁决 = 钱路/状态闭环(退款/罚没/completed)→ 与 admin fallback 一致 fail-closed:
75
+ // token 必须【显式绑定】本案 dispute_id + 本结果 decision,绝不允许 null/未绑定的泛化 arbitrate token 跨案裁决。
76
+ // 通用争议维持既有(d==null 允许);resolver 只对两选 ruling 执行,故钱路仅在严格绑定时可达。
77
+ const _dcRuling = req.body?.ruling === 'decline_no_fault_upheld' || req.body?.ruling === 'decline_fault_confirmed';
136
78
  const hpCheck = requireHumanPresence(user.id, 'arbitrate', req.body?.webauthn_token, 'require_human_presence_for_arbitrate', (data) => {
137
79
  const d = data;
80
+ if (_dcRuling)
81
+ return d != null && typeof d === 'object' && d.dispute_id === req.params.id && d.decision === req.body?.ruling;
138
82
  return d == null || d.dispute_id === req.params.id;
139
83
  });
140
84
  if (!hpCheck.ok)
@@ -142,6 +86,34 @@ export function registerDisputesWriteRoutes(app, deps) {
142
86
  const { ruling, reason, refund_amount, liable_party_id, liability_parties } = req.body;
143
87
  if (!ruling || !reason)
144
88
  return void res.json({ error: '请提供裁定结果(ruling)和理由(reason)' });
89
+ // P1-1:先读 dispute,再按 dispute_type 选 ruling allowlist —— 否则 decline_contest 的专用枚举会被通用校验提前 400。
90
+ const dispute = await getDisputeDetails(db, req.params.id);
91
+ if (!dispute)
92
+ return void res.status(404).json({ error: '争议不存在' });
93
+ // PR3:decline_contest → 唯一 domain resolver(两选;dispute CAS + COI + assignment + 终态 completed + 结算 + 审计,单事务失败全回滚)。
94
+ if (dispute.dispute_type === 'decline_contest') {
95
+ const dcAllow = ['decline_no_fault_upheld', 'decline_fault_confirmed'];
96
+ if (!dcAllow.includes(ruling))
97
+ return void errorRes(res, 400, 'BAD_DECISION', `拒单举证仲裁的 ruling 必须是 ${dcAllow.join(' / ')} 之一`);
98
+ try {
99
+ const r = resolveDeclineContestDispute(db, req.params.id, user.id, ruling, reason, 'arbitrator');
100
+ try {
101
+ notifyDeclineContestResolved(db, r.orderId, r.decision);
102
+ }
103
+ catch (e) {
104
+ console.warn('[notify] dc-resolve:', e.message);
105
+ }
106
+ try {
107
+ logAdminAction(user.id, `decline_contest_${r.decision}`, 'order', r.orderId, { source: 'arbitrator', dispute_id: req.params.id, reason });
108
+ }
109
+ catch { /* */ }
110
+ return void res.json({ success: true, outcome: r.decision, order_status: 'completed' });
111
+ }
112
+ catch (e) {
113
+ const err = e;
114
+ return void errorRes(res, err.http || 400, err.code || 'DC_RESOLVE_FAILED', err.message || '裁决失败');
115
+ }
116
+ }
145
117
  const validRulings = ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split', 'dismiss_to_negotiation'];
146
118
  if (!validRulings.includes(ruling)) {
147
119
  return void res.json({ error: `ruling 必须是 ${validRulings.join(' / ')} 之一` });
@@ -156,9 +128,6 @@ export function registerDisputesWriteRoutes(app, deps) {
156
128
  }
157
129
  }
158
130
  }
159
- const dispute = await getDisputeDetails(db, req.params.id);
160
- if (!dispute)
161
- return void res.status(404).json({ error: '争议不存在' });
162
131
  // PR-B COI 硬门:当事方(买家/卖家/物流/发起人/被诉人)不得仲裁本案。在领取/裁定前拦截。
163
132
  if (arbitratorHasConflict(db, dispute.order_id, dispute.initiator_id ?? null, dispute.defendant_id ?? null, user.id)) {
164
133
  return void res.status(403).json({ error: '你是本案当事方,不可仲裁(利益冲突)', error_code: 'ARBITRATOR_CONFLICT_OF_INTEREST' });