@seasonkoh/webaz 0.1.30 → 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 (33) 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 +202 -19
  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 +78 -23
  23. package/dist/pwa/routes/admin-reports.js +40 -3
  24. package/dist/pwa/routes/agent-grants.js +147 -14
  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/routes/products-create.js +33 -13
  29. package/dist/pwa/routes/webauthn.js +1 -1
  30. package/dist/pwa/server.js +7 -7
  31. package/dist/runtime/agent-grant-scopes.js +26 -8
  32. package/dist/runtime/webaz-schema-helpers.js +28 -0
  33. package/package.json +15 -2
@@ -0,0 +1,20 @@
1
+ // RFC-020 — shared grant-duration picker (used by #pair initial pairing + #agent-approvals expansion).
2
+ // Classic script, loaded before the pages that use it. Globals (t / escHtml) resolve at call time.
3
+ // The human chooses the grant lifetime; safe scopes → up to 30d. Kept in one place so both approval
4
+ // surfaces stay identical and the pinned page files don't grow.
5
+ ;(function () {
6
+ const LABEL = { '1h': '1 小时', '24h': '24 小时', '7d': '7 天', '30d': '30 天' } // 'once' intentionally omitted — no real single-use grant yet (would become a misleading 1h reusable bearer)
7
+
8
+ // Render a <select id=selectId> of `allowed` durations, pre-selecting `suggested`. Falls back to a safe set.
9
+ window.grantDurationSelect = function (allowed, suggested, selectId) {
10
+ const opts = (Array.isArray(allowed) && allowed.length) ? allowed : ['1h', '24h', '7d', '30d']
11
+ const sel = suggested || '7d'
12
+ return `<div style="margin-top:10px"><span style="font-size:12px;color:#6b7280">${t('授权时长')}(${t('你可以改')}):</span>`
13
+ + `<select id="${escHtml(String(selectId))}" style="width:100%;margin-top:4px;padding:8px;border:1px solid #d1d5db;border-radius:8px;font-size:14px">`
14
+ + opts.map(d => `<option value="${escHtml(String(d))}" ${d === sel ? 'selected' : ''}>${escHtml(t(LABEL[d] || String(d)))}</option>`).join('')
15
+ + `</select></div>`
16
+ }
17
+
18
+ // Read the human's chosen duration from a rendered selector (undefined if absent → backend uses its default).
19
+ window.grantDurationValue = function (selectId) { const el = document.getElementById(selectId); return (el && el.value) || undefined }
20
+ })()
@@ -566,7 +566,7 @@ async function render(page, params) {
566
566
  const { status, body } = await apiWithStatus('GET', '/me')
567
567
  if (status === 200 && body && !body.error) {
568
568
  state.user = body
569
- connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800); if (window.arbEntryHydrate) arbEntryHydrate()
569
+ connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800); if (window.arbEntryHydrate) arbEntryHydrate(); if (window.refreshArbBadge) refreshArbBadge()
570
570
  try { p2pStart() } catch (e) { console.warn('[P2P] start err', e) }
571
571
  } else if (status === 401 || status === 403) {
572
572
  // 真·鉴权失败:清掉两层存储
@@ -1073,7 +1073,7 @@ function shell(content, activeTab, opts) {
1073
1073
  ]
1074
1074
  } else if (role === 'arbitrator') {
1075
1075
  tabs = [
1076
- { id: 'seller', icon: '⚖️', label: t('仲裁台') },
1076
+ { id: 'seller', icon: '⚖️', label: t('仲裁台'), arbBadge: true },
1077
1077
  { id: 'orders', icon: '📋', label: t('记录') },
1078
1078
  { id: 'chats', target: 'chats/notifs', icon: '💬', label: t('消息'), chatsBadge: true },
1079
1079
  { id: 'wallet', icon: '💰', label: t('钱包') },
@@ -1204,7 +1204,7 @@ function shell(content, activeTab, opts) {
1204
1204
  <span class="tab-icon" style="position:relative">
1205
1205
  ${tb.icon}
1206
1206
  ${tb.badge ? `<span id="notif-badge" style="position:absolute;top:-4px;right:-6px;background:#dc2626;color:#fff;border-radius:99px;font-size:10px;padding:0 4px;min-width:16px;text-align:center;display:${state.unread > 0 ? 'inline' : 'none'}">${state.unread || ''}</span>` : ''}
1207
- ${tb.chatsBadge ? (() => { const _agg = (state.chatsUnread||0)+(state.unread||0)+(state.announcementsUnread||0)+(state.feedbackUnread||0); return `<span class="chats-badge" style="position:absolute;top:-4px;right:-6px;background:#4f46e5;color:#fff;border-radius:99px;font-size:10px;padding:0 4px;min-width:16px;text-align:center;display:${_agg > 0 ? 'inline' : 'none'}">${_agg || ''}</span>` })() : ''}
1207
+ ${tb.chatsBadge ? (() => { const _agg = (state.chatsUnread||0)+(state.unread||0)+(state.announcementsUnread||0)+(state.feedbackUnread||0); return `<span class="chats-badge" style="position:absolute;top:-4px;right:-6px;background:#4f46e5;color:#fff;border-radius:99px;font-size:10px;padding:0 4px;min-width:16px;text-align:center;display:${_agg > 0 ? 'inline' : 'none'}">${_agg || ''}</span>` })() : ''}${tb.arbBadge ? `<span class="arb-badge" style="position:absolute;top:-4px;right:-6px;background:#7c3aed;color:#fff;border-radius:99px;font-size:10px;padding:0 4px;min-width:16px;text-align:center;display:${(state.arbPending||0) > 0 ? 'inline' : 'none'}">${state.arbPending || ''}</span>` : ''}
1208
1208
  </span>${tb.label}
1209
1209
  </button>`).join('')}
1210
1210
  </nav>`}`
@@ -13974,7 +13974,7 @@ function buildDisputeHtml(dispute, user) {
13974
13974
  // ── 仲裁员:暂停 / 恢复自动判定时钟(playbook §2.1)────
13975
13975
  // 补证据期 > 48h 时必须显式暂停,避免被协议层 48h 沉默判架空
13976
13976
  const isPaused = dispute.auto_judge_paused_until && dispute.auto_judge_paused_until * 1000 > Date.now()
13977
- const pauseSection = isArbitrator && dispute.status !== 'resolved' && !dispute.ruling_type ? (
13977
+ const pauseSection = isArbitrator && dispute.dispute_type !== 'decline_contest' && dispute.status !== 'resolved' && !dispute.ruling_type ? (
13978
13978
  isPaused ? `
13979
13979
  <div style="margin-top:12px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:12px">
13980
13980
  <div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:6px">⏸️ ${t('自动判定时钟已冻结')}</div>
@@ -14011,7 +14011,7 @@ function buildDisputeHtml(dispute, user) {
14011
14011
  ) : ''
14012
14012
 
14013
14013
  // ── 仲裁员:发起补充证据请求 ──────────────────
14014
- const requestEvidenceSection = isArbitrator && dispute.status !== 'resolved' ? `
14014
+ const requestEvidenceSection = isArbitrator && dispute.dispute_type !== 'decline_contest' && dispute.status !== 'resolved' ? `
14015
14015
  <div style="margin-top:12px">
14016
14016
  <button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="(function(){var s=document.getElementById('req-ev-section');s.style.display=s.style.display==='none'?'':'none'})()">📋 请求补充证据</button>
14017
14017
  <div id="req-ev-section" style="display:none;margin-top:10px;background:#f8fafc;border-radius:8px;padding:12px">
@@ -14050,7 +14050,7 @@ function buildDisputeHtml(dispute, user) {
14050
14050
  </div>` : ''
14051
14051
 
14052
14052
  // ── 仲裁员裁定(含责任分配)────────────────────
14053
- const arbitrateSection = isArbitrator && (dispute.status === 'open' || dispute.status === 'in_review') ? `
14053
+ const arbitrateSection = isArbitrator && (dispute.status === 'open' || dispute.status === 'in_review') ? (dispute.dispute_type === 'decline_contest' ? (window.dcRulingForm ? window.dcRulingForm(dispute) : '') : `
14054
14054
  <div style="margin-top:12px;border-top:1px solid #fecaca;padding-top:12px">
14055
14055
  <div style="font-weight:600;font-size:13px;margin-bottom:8px">⚖️ 仲裁员裁定(截止 ${fmtTime(dispute.arbitrate_deadline)})</div>
14056
14056
  ${window.dpArbFeeNote ? window.dpArbFeeNote(dispute.payment_rail) : ''}
@@ -14102,7 +14102,7 @@ function buildDisputeHtml(dispute, user) {
14102
14102
  <textarea class="form-control" id="arb-reason" placeholder="${t('请详细说明裁定依据(将记录在案)')}" style="font-size:13px"></textarea>
14103
14103
  </div>
14104
14104
  <button class="btn btn-danger btn-sm" style="width:auto" onclick="handleArbitrate('${dispute.id}')">${t('确认裁定')}</button>
14105
- </div>` : ''
14105
+ </div>`) : ''
14106
14106
  // ── 裁定结果已在 timeline 末尾以 'ruling' / 'resolved' 事件呈现(W4)
14107
14107
 
14108
14108
  // W4: 三方参与者头部(取代两行 detail-row,更紧凑)
@@ -14342,7 +14342,7 @@ window.handleArbitrate = async (disputeId) => {
14342
14342
  body = { ...body, liability_parties: liabilityParties, refund_amount: totalCalc }
14343
14343
  }
14344
14344
 
14345
- let _arbTk; try { _arbTk = await requestPasskeyGate('arbitrate', { dispute_id: disputeId }) } catch (e) { msgEl.innerHTML = alert$('error', (e && e.message ? e.message + ' — ' : '') + t('仲裁裁定需现场真人 Passkey 验证')); return }; msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>` // 所有 ruling(含驳回)先取 arbitrate gate token
14345
+ let _arbTk; try { _arbTk = await requestPasskeyGate('arbitrate', { dispute_id: disputeId, decision: ruling }) } catch (e) { msgEl.innerHTML = alert$('error', (e && e.message ? e.message + ' — ' : '') + t('仲裁裁定需现场真人 Passkey 验证')); return }; msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>` // 所有 ruling(含驳回)先取 arbitrate gate token;绑 dispute_id + decision(decline_contest 钱路 fail-closed 校验)
14346
14346
  const res = await POST(`/disputes/${disputeId}/arbitrate`, { ...body, webauthn_token: _arbTk })
14347
14347
  if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
14348
14348
 
@@ -14622,7 +14622,7 @@ async function renderDisputeList(app) {
14622
14622
  <div class="card" onclick="navigate('#dispute/${d.id}')" style="padding:10px 12px;display:flex;align-items:center;gap:10px;cursor:pointer;${urgent ? 'border-left:3px solid #dc2626;' : ''}">
14623
14623
  <div style="font-size:22px;flex-shrink:0">${urgent ? '⏰' : stIcon}</div>
14624
14624
  <div style="flex:1;min-width:0">
14625
- <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(d.reason || '')}</div>
14625
+ <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${window.dcChip ? window.dcChip(d) : ''}${escHtml(d.reason || '')}</div>
14626
14626
  <div style="font-size:11px;color:#6b7280;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(d.initiator_name || '?')} → ${escHtml(d.defendant_name || '—')} · ${d.total_amount} WAZ</div>
14627
14627
  <div style="font-size:10px;color:${urgent ? '#dc2626' : '#9ca3af'};margin-top:2px${urgent ? ';font-weight:600' : ''}">${disputeStatusLabelText(d.status)}${deadline ? ` · ${urgent ? '⚠ ' : ''}${t('截止')} ${fmtTime(deadline)}` : ` · ${fmtTime(d.updated_at || d.created_at)}`}</div>
14628
14628
  </div>
@@ -17,6 +17,29 @@ window.resolveInitialLang = (savedLang, navigatorLike) => {
17
17
  window._lang = window.resolveInitialLang(localStorage.getItem('webaz_lang'), typeof navigator !== 'undefined' ? navigator : null)
18
18
 
19
19
  const _EN = {
20
+ // ── RFC-021 order-action 审批卡(app-agent-approvals-order.js)──
21
+ '对订单': 'On order',
22
+ '批准后由服务端在你授权下执行;agent 不直接执行': 'On approval the server executes under your authority; the agent never executes directly',
23
+ // ── 统一仲裁台:decline_contest(拒单举证仲裁,app-decline-contest-ui.js)──
24
+ '拒单举证仲裁': 'Decline-contest arbitration',
25
+ '裁决通道即将开放': 'Ruling channel opening soon',
26
+ '卖家主张客观无责并已举证。裁决通道即将开放——届时可裁定:维持无责(全退买家+退卖家质押)或驳回(判卖家违约)。': 'The seller claims objective no-fault and has submitted evidence. The ruling channel is opening soon — an arbitrator will then rule: uphold no-fault (full refund to buyer + seller stake returned) or reject (seller at fault).',
27
+ '新的拒单举证仲裁待处理': 'New decline-contest arbitration pending',
28
+ '一笔卖家客观拒单举证已进入统一仲裁台,等待仲裁员裁决。': 'A seller objective-decline contest has entered the unified arbitration desk, awaiting an arbitrator ruling.',
29
+ '拒单举证仲裁裁决': 'Decline-contest ruling',
30
+ '维持无责 —— 全退买家 + 退卖家质押,零罚没': 'Uphold no-fault — full refund to buyer + seller stake returned, no forfeit',
31
+ '驳回举证,判卖家违约 —— 退款买家 + 罚没质押': 'Reject the contest, seller at fault — refund buyer + forfeit stake',
32
+ '裁定理由(必填)': 'Ruling reason (required)',
33
+ '兜底·维持无责': 'Fallback · uphold no-fault',
34
+ '兜底·判违约': 'Fallback · seller at fault',
35
+ '此操作需现场真人 Passkey 验证': 'This action requires a live human Passkey verification',
36
+ '裁决已执行': 'Ruling executed',
37
+ '拒单举证仲裁已超时,请尽快裁决': 'Decline-contest arbitration overdue — please rule soon',
38
+ '一笔拒单举证仲裁已过仲裁窗口仍未裁决,已进入 admin 兜底窗口 —— 请尽快处理,否则将自动判卖家违约。': 'A decline-contest arbitration has passed its ruling window unresolved and entered the admin fallback window — please handle it soon, or the seller will be auto-ruled at fault.',
39
+ '拒单举证仲裁:维持无责': 'Decline-contest: no-fault upheld',
40
+ '拒单举证仲裁:驳回,判卖家违约': 'Decline-contest: rejected, seller at fault',
41
+ '仲裁认定卖家客观无责:买家已全额退款,卖家质押已退回,无罚没。订单已结。': 'Arbitration found the seller objectively no-fault: buyer fully refunded, seller stake returned, no forfeit. Order closed.',
42
+ '仲裁驳回卖家举证,判卖家违约:买家已退款,卖家质押按违约处置。订单已结。': 'Arbitration rejected the seller\'s contest and ruled seller at fault: buyer refunded, seller stake handled per fault rules. Order closed.',
20
43
  // ── 仲裁员管理 admin UI(app-arbitrator-admin.js,PR-F)──
21
44
  '仲裁员管理': 'Arbitrator management',
22
45
  '授权真人仲裁员(唯一授权源=active 白名单)。目标须已注册 Passkey、非本案当事人、非 agent/系统账号。授权/暂停/撤销均需你现场 Passkey,后端留痕。': 'Grant real human arbitrators (the sole authorization source is the active whitelist). The target must have a registered Passkey, not be a party to the case, and not be an agent/system account. Grant/suspend/revoke each require your live Passkey and are audited server-side.',
@@ -556,11 +579,11 @@ const _EN = {
556
579
  '当你的 agent 请求更多权限时,会出现在这里等你批准。': 'When your agent requests more permissions, they appear here for your approval.',
557
580
  '它想做什么': 'What it wants to do',
558
581
  '授权时长': 'Grant duration',
582
+ '你可以改': 'you can change this',
559
583
  '请求于': 'Requested',
560
584
  '低风险': 'Low risk',
561
585
  '中风险': 'Medium risk',
562
586
  '高风险': 'High risk',
563
- '一次性': 'One-time',
564
587
  '1 小时': '1 hour',
565
588
  '7 天': '7 days',
566
589
  '确认拒绝这个授权请求?': 'Reject this permission request?',
@@ -60,9 +60,11 @@
60
60
  <script src="/app-price.js"></script>
61
61
  <script src="/app-order-labels.js"></script>
62
62
  <script src="/app-seller.js"></script>
63
+ <script src="/app-grant-duration.js"></script>
63
64
  <script src="/app-agents.js"></script>
64
65
  <script src="/app-agent-pair.js"></script>
65
66
  <script src="/app-agent-approvals.js"></script>
67
+ <script src="/app-agent-approvals-order.js"></script>
66
68
  <script src="/app-agent-appeal.js"></script>
67
69
  <script src="/app-direct-pay.js"></script>
68
70
  <script src="/app-direct-pay-readiness.js"></script>
@@ -107,6 +109,8 @@
107
109
  <script src="/app-order-errors.js"></script>
108
110
  <script src="/app-arbitrator-entry.js"></script>
109
111
  <script src="/app-arbitrator-admin.js"></script>
112
+ <script src="/app-decline-contest-ui.js"></script>
113
+ <script src="/app-decline-contest-ruling.js"></script>
110
114
  <script src="/app-contribution-hub.js"></script>
111
115
  <script src="/app-prelaunch-waz.js"></script>
112
116
  <script src="/app-chat-poll.js"></script>
@@ -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 (776 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.",
@@ -2774,6 +2808,16 @@
2774
2808
  }
2775
2809
  ],
2776
2810
  "x-webaz-grant-scope": "seller_products_read"
2811
+ },
2812
+ "post": {
2813
+ "summary": "POST /api/agent/seller/products",
2814
+ "tags": [],
2815
+ "security": [
2816
+ {
2817
+ "grantBearer": []
2818
+ }
2819
+ ],
2820
+ "x-webaz-grant-scope": "seller_product_draft"
2777
2821
  }
2778
2822
  },
2779
2823
  "/api/agent/verifiability": {
@@ -3017,6 +3061,17 @@
3017
3061
  ]
3018
3062
  }
3019
3063
  },
3064
+ "/api/arbitrator/pending-count": {
3065
+ "get": {
3066
+ "summary": "非仲裁员返回 0(不报错,前端 badge 拉取对所有人无害)。",
3067
+ "tags": [],
3068
+ "security": [
3069
+ {
3070
+ "bearerAuth": []
3071
+ }
3072
+ ]
3073
+ }
3074
+ },
3020
3075
  "/api/arbitrator/status": {
3021
3076
  "get": {
3022
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)