@seasonkoh/webaz 0.1.29 → 0.1.31

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 (126) hide show
  1. package/dist/bond-refund-blockers.js +50 -0
  2. package/dist/bond-slash.js +100 -0
  3. package/dist/bond-terms.js +31 -0
  4. package/dist/direct-pay-bond-rail-clearance.js +10 -5
  5. package/dist/direct-pay-cancel-refund.js +160 -0
  6. package/dist/direct-pay-create.js +78 -9
  7. package/dist/direct-pay-disclosures.js +17 -11
  8. package/dist/direct-pay-fee-ar.js +2 -2
  9. package/dist/direct-pay-fee-prepay-request.js +80 -0
  10. package/dist/direct-pay-returns.js +104 -0
  11. package/dist/direct-pay-stock.js +9 -0
  12. package/dist/direct-receive-deferral.js +30 -1
  13. package/dist/direct-receive-deposits.js +122 -17
  14. package/dist/free-shipping.js +37 -0
  15. package/dist/fx-rates.js +7 -0
  16. package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
  17. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  18. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
  19. package/dist/layer1-agent/L1-1-mcp-server/server.js +150 -33
  20. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  21. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  22. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  23. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  24. package/dist/platform-receive-accounts.js +94 -0
  25. package/dist/pwa/arbitration-read-admin.js +37 -0
  26. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  27. package/dist/pwa/contract-fingerprint.js +15 -0
  28. package/dist/pwa/direct-pay-order-redaction.js +20 -2
  29. package/dist/pwa/human-presence.js +2 -6
  30. package/dist/pwa/public/app-account.js +9 -9
  31. package/dist/pwa/public/app-admin-disputes.js +55 -0
  32. package/dist/pwa/public/app-agent-appeal.js +90 -0
  33. package/dist/pwa/public/app-agent-approvals.js +93 -0
  34. package/dist/pwa/public/app-agent-pair.js +127 -0
  35. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  36. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  37. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  38. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  39. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  40. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  41. package/dist/pwa/public/app-bond-ui.js +108 -0
  42. package/dist/pwa/public/app-chat-poll.js +6 -6
  43. package/dist/pwa/public/app-contribution-hub.js +23 -0
  44. package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
  45. package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
  46. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  47. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  48. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  49. package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
  50. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  51. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  52. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  53. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  54. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  55. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  56. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  57. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  58. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  59. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  60. package/dist/pwa/public/app-direct-pay.js +36 -37
  61. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  62. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  63. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  64. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  65. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  66. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  67. package/dist/pwa/public/app-notif-templates.js +22 -0
  68. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  69. package/dist/pwa/public/app-order-errors.js +50 -0
  70. package/dist/pwa/public/app-order-labels.js +19 -0
  71. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  72. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  73. package/dist/pwa/public/app-poll-governor.js +22 -0
  74. package/dist/pwa/public/app-profile.js +4 -4
  75. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  76. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  77. package/dist/pwa/public/app-seller.js +1 -1
  78. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  79. package/dist/pwa/public/app.js +133 -160
  80. package/dist/pwa/public/i18n.js +675 -5
  81. package/dist/pwa/public/index.html +41 -0
  82. package/dist/pwa/public/openapi.json +719 -11
  83. package/dist/pwa/public/style.css +3 -0
  84. package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
  85. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  86. package/dist/pwa/routes/admin-reports.js +31 -5
  87. package/dist/pwa/routes/agent-governance.js +1 -1
  88. package/dist/pwa/routes/agent-grants.js +281 -32
  89. package/dist/pwa/routes/analytics.js +2 -0
  90. package/dist/pwa/routes/arbitrator.js +67 -14
  91. package/dist/pwa/routes/bond-seller.js +162 -0
  92. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  93. package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
  94. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  95. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  96. package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
  97. package/dist/pwa/routes/disputes-read.js +20 -6
  98. package/dist/pwa/routes/disputes-write.js +91 -33
  99. package/dist/pwa/routes/external-anchors.js +4 -2
  100. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  101. package/dist/pwa/routes/governance-onboarding.js +46 -8
  102. package/dist/pwa/routes/logistics.js +4 -4
  103. package/dist/pwa/routes/me-data.js +2 -2
  104. package/dist/pwa/routes/mutual-cancel.js +62 -0
  105. package/dist/pwa/routes/orders-action.js +182 -9
  106. package/dist/pwa/routes/orders-create.js +18 -7
  107. package/dist/pwa/routes/orders-read.js +76 -14
  108. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  109. package/dist/pwa/routes/products-create.js +45 -16
  110. package/dist/pwa/routes/products-update.js +15 -1
  111. package/dist/pwa/routes/profile-identity.js +4 -2
  112. package/dist/pwa/routes/returns.js +39 -8
  113. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  114. package/dist/pwa/routes/seller-quota.js +5 -1
  115. package/dist/pwa/routes/shipping-templates.js +219 -0
  116. package/dist/pwa/routes/snf.js +4 -1
  117. package/dist/pwa/routes/webauthn.js +3 -3
  118. package/dist/pwa/server.js +63 -40
  119. package/dist/runtime/agent-grant-scopes.js +69 -1
  120. package/dist/runtime/webaz-schema-helpers.js +57 -3
  121. package/dist/sale-regions.js +116 -0
  122. package/dist/shipping-templates.js +119 -0
  123. package/dist/trade-tax.js +99 -0
  124. package/dist/trade-terms.js +76 -0
  125. package/dist/version.js +1 -1
  126. package/package.json +61 -2
@@ -41,6 +41,14 @@ import { ensureEvidenceColumns, uploadEvidence, listEvidence as listEvidenceFile
41
41
  import { cleanupOrphanNotePhotos, } from '../layer2-business/L2-notes/note-photo-storage.js';
42
42
  import { initAnchorRegistrySchema, retireAnchorsByTarget, reclaimRetiredAnchors, retireIdleAnchors, userReferralVolume, computeTierLetter, } from '../layer2-business/L2-anchor-registry/anchor-registry.js';
43
43
  import { initDisputeSchema, createDispute, respondToDispute, arbitrateDispute, getOrderDispute, getDisputeDetails, getOpenDisputes, checkDisputeTimeouts, initEvidenceRequestSchema, requestEvidence, getEvidenceRequests, addPartyEvidence, } from '../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
44
+ import { initMutualCancelSchema } from '../layer3-trust/L3-1-dispute-engine/mutual-cancel.js';
45
+ import { registerMutualCancelRoutes } from './routes/mutual-cancel.js';
46
+ import { initDirectPayCancelRefundSchema } from '../direct-pay-cancel-refund.js';
47
+ import { registerDirectPayCancelRefundRoutes } from './routes/direct-pay-cancel-refund.js';
48
+ import { registerDirectPayReturnsRoutes } from './routes/direct-pay-returns.js';
49
+ import { registerDirectPayPendingAcceptRoutes } from './routes/direct-pay-pending-accept.js';
50
+ import { registerShippingTemplateRoutes } from './routes/shipping-templates.js';
51
+ import { registerBondSellerRoutes } from './routes/bond-seller.js'; // 协商取消(disputed) + 直付取消退款握手(accepted):域 schema + 端点
44
52
  import { initNotificationSchema, notifyTransition, setPushCallback, scanDeadlineReminders, } from '../layer2-business/L2-6-notifications/notification-engine.js';
45
53
  import { initSkillMarketSchema } from '../layer4-economics/L4-4-skill-market/skill-listing-engine.js';
46
54
  import { computeAgentPassport } from '../layer1-agent/L1-2-identity/agent-passport.js';
@@ -154,6 +162,7 @@ import { registerExternalAnchorsRoutes } from './routes/external-anchors.js';
154
162
  import { registerAnchorsRoutes } from './routes/anchors.js';
155
163
  // 仲裁员申请 (#1013 Phase 44) — 4 user + 3 admin endpoints
156
164
  import { registerArbitratorRoutes } from './routes/arbitrator.js';
165
+ import { isEligibleArbitrator as isEligibleArbitratorImpl } from './arbitrator-lifecycle.js'; // PR-B: active-whitelist-only 授权源
157
166
  // Governance onboarding (W3.5-B 实施 #1093 阶段 1) — 2 user endpoints
158
167
  import { registerGovernanceOnboardingRoutes } from './routes/governance-onboarding.js';
159
168
  import { startAutoDeactivateCron, runAutoDeactivateSweep } from './routes/governance-auto-deactivate.js';
@@ -250,11 +259,15 @@ import { registerOrdersActionRoutes } from './routes/orders-action.js';
250
259
  import { registerOrdersCreateRoutes } from './routes/orders-create.js';
251
260
  import { registerDirectPayDisclosureAckRoutes } from './routes/direct-pay-disclosure-acks.js'; // PR-4d: Direct Pay 风险披露 ack 端点(薄 adapter)
252
261
  import { registerDirectReceivePaymentInstructionRoutes } from './routes/direct-receive-payment-instructions.js';
253
- import { registerDirectReceiveAccountsRoutes } from './routes/direct-receive-accounts.js'; // PR-4f-a instruction CRUD + Phase C1 multi-account+QR
254
- import { registerDirectPayAvailabilityRoutes } from './routes/direct-pay-availability.js'; // PR-4a: Direct Pay 可用性只读(控制面 SSOT)
255
- import { registerAdminDirectReceiveDepositsRoutes } from './routes/admin-direct-receive-deposits.js'; // PR-4b-3: ROOT 生产保证金 receipt 确认(fail-closed scaffold)
262
+ import { registerDirectReceiveAccountsRoutes } from './routes/direct-receive-accounts.js';
263
+ import { registerFeePrepayRequestRoutes } from './routes/fee-prepay-requests.js'; // PR-4f-a instruction CRUD + Phase C1 multi-account+QR + 平台服务费预充值申请(卖家侧)
264
+ import { registerDirectPayAvailabilityRoutes } from './routes/direct-pay-availability.js';
265
+ import { registerSellerDirectPayReportRoutes } from './routes/seller-directpay-report.js'; // PR-4a 可用性 + 卖家直付销售统计/对账/平台费明细(只读)
266
+ import { registerAdminDirectReceiveDepositsRoutes } from './routes/admin-direct-receive-deposits.js';
267
+ import { registerPlatformReceiveAccountsRoutes } from './routes/platform-receive-accounts.js'; // PR-4b-3 ROOT 生产保证金 + 平台多收款方式 admin 管理(fee 预充值申请流前置)
256
268
  // Disputes 读端点 (#1013 Phase 86) — 5 endpoints (list/similar/detail/evidence-list/parties)
257
269
  import { registerDisputesReadRoutes } from './routes/disputes-read.js';
270
+ import { isArbitrationReadAdmin } from './arbitration-read-admin.js'; // 争议详情只读 admin 门(与后台列表授权一致)
258
271
  // Disputes 写端点 (#1013 Phase 87) — 5 endpoints (respond/arbitrate/add-evidence/evidence-blob/request-evidence)
259
272
  import { registerDisputesWriteRoutes } from './routes/disputes-write.js';
260
273
  // Products 声明 (#1013 Phase 88) — 2 endpoints (POST claim + GET claims)
@@ -270,7 +283,7 @@ import { registerProductsCrudRoutes } from './routes/products-crud.js';
270
283
  // Products PUT update (#1013 Phase 93) — 1 endpoint (123 行)
271
284
  import { registerProductsUpdateRoutes } from './routes/products-update.js';
272
285
  // Products POST create (#1013 Phase 94) — 1 endpoint (232 行)
273
- import { registerProductsCreateRoutes } from './routes/products-create.js';
286
+ import { registerProductsCreateRoutes, makeCreateProductHandler } from './routes/products-create.js';
274
287
  // Products GET list (#1013 Phase 95) — 1 endpoint (399 行)
275
288
  import { registerProductsListRoutes } from './routes/products-list.js';
276
289
  // P2P 商品 (#1013 Phase 96) — 5 endpoints
@@ -395,6 +408,8 @@ const settleClaimTask = (taskId) => settleClaimTaskRaw(db, generateId, taskId);
395
408
  const notifyEligibleVerifiers = (args) => notifyEligibleVerifiersRaw(db, generateId, args);
396
409
  initSystemUser(db);
397
410
  initDisputeSchema(db);
411
+ initMutualCancelSchema(db);
412
+ initDirectPayCancelRefundSchema(db); // + mutual_cancel_proposals / direct_pay_cancel_requests 表
398
413
  initNotificationSchema(db);
399
414
  initSkillSchema(db);
400
415
  initSkillMarketSchema(db);
@@ -843,10 +858,10 @@ const DEFAULT_PARAMS = [
843
858
  // ⚠️ Codex #111:stake-required 模式(=1)【尚未实现】—— 下单不锁 stake、settleFault 仍按 stake_backing=0 不没收,
844
859
  // 开启会给出虚假"真没收"协议语义。故 max 锁 0(不可开启);待 Phase 3 钱路径迁移实现真锁(下单原子锁 balance→staked)再放开。
845
860
  { key: 'require_seller_stake', value: '0', type: 'number', description: 'RFC-008 是否要求卖家质押(0=起步免赔付/零门槛)。⚠️ stake-required(=1)未实现、暂锁 0 不可开启,见 Phase 3', category: 'fee', min: 0, max: 0 },
846
- // RFC-008 stage 2:违约罚没率,【与质押率解耦】(低质押=低摩擦 + 高罚没=强威慑,单一费率做不到)。
847
- // 背书订单:penalty = fault_penalty_rate × total,先扣 staked(封顶背书)再扣自由 balance(责任自负,真可执行)。
861
+ // RFC-008 stage 2:违约罚没率,【与质押率解耦】(低质押=低摩擦+高罚没=强威慑)。背书订单:penalty=rate×total,先扣 staked(封顶背书)再扣自由 balance(责任自负)。
848
862
  // 起步免赔付(stake_backing=0):仍 0 没收,绝不碰新商家自由余额。settleFault 按订单 stake_backing 判定。
849
863
  { key: 'fault_penalty_rate', value: '0.30', type: 'number', description: 'RFC-008 违约罚没率(与质押率解耦;背书订单 staked 不足扣自由 balance;起步免赔付订单不适用)', category: 'fee', min: 0, max: 0.50 },
864
+ { key: 'trade.platform_region_blocklist', value: '[]', type: 'json', description: '跨境 S1 平台合规禁售地区(JSON 数组大写区码,如 ["KP"]):建单 PRODUCT_RESTRICTED 硬拒,商家不可放宽;默认空。admin PATCH 有 JSON+区码校验', category: 'system' },
850
865
  // RFC-007 stage 3:客观理由拒单的【举证窗口】小时数。卖家声称客观无责拒单 → 临时判责,此窗口内可开仲裁(stage 5)举证;
851
866
  // 到期无人仲裁 → 自动终结为违约。窗口内买家 escrow 暂不退(随终结/翻案一次性结算),0=不给窗口(直接违约)。
852
867
  { key: 'decline_contest_window_hours', value: '24', type: 'number', description: 'RFC-007 客观拒单举证窗口(小时);到期未仲裁则终结为违约', category: 'limit', min: 0, max: 168 },
@@ -1373,11 +1388,7 @@ try {
1373
1388
  }
1374
1389
  catch { }
1375
1390
  // 2026-05-22: 复合索引 — GET /api/shares/dashboard bought_products 5 子查询性能优化
1376
- // note_count / first_note_id 用:owner_id + related_order_id + type + status
1377
- try {
1378
- db.exec("CREATE INDEX IF NOT EXISTS idx_share_owner_order_type ON shareables(owner_id, related_order_id, type, status)");
1379
- }
1380
- catch { }
1391
+ // note_count / first_note_id 用的 idx_share_owner_order_type 已移至下方 related_order_id ALTER 之后(此处列尚不存在,先建=fresh 库永缺该索引)
1381
1392
  // product_share_count 用:owner_id + related_product_id + status(避免回表)
1382
1393
  try {
1383
1394
  db.exec("CREATE INDEX IF NOT EXISTS idx_share_owner_product ON shareables(owner_id, related_product_id, status)");
@@ -1423,6 +1434,10 @@ try {
1423
1434
  db.exec("CREATE INDEX IF NOT EXISTS idx_share_order ON shareables(related_order_id) WHERE related_order_id IS NOT NULL");
1424
1435
  }
1425
1436
  catch { }
1437
+ try {
1438
+ db.exec("CREATE INDEX IF NOT EXISTS idx_share_owner_order_type ON shareables(owner_id, related_order_id, type, status)");
1439
+ }
1440
+ catch { }
1426
1441
  // 笔记图片 hash 索引 / Wave A 购物表(心愿单 · 商品Q&A · 优惠券)→ server-schema.ts
1427
1442
  // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1428
1443
  initNotePhotoIndexSchema(db);
@@ -1668,13 +1683,12 @@ catch { }
1668
1683
  for (const stmt of [
1669
1684
  'ALTER TABLE products ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1670
1685
  'ALTER TABLE secondhand_items ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1671
- 'ALTER TABLE auctions ADD COLUMN claim_loss_count INTEGER DEFAULT 0',
1672
1686
  ]) {
1673
1687
  try {
1674
1688
  db.exec(stmt);
1675
1689
  }
1676
1690
  catch { }
1677
- }
1691
+ } // auctions 的同名 ALTER 在 auctions CREATE 之后(此处表尚不存在,先跑=fresh 库永缺列 — 铁律:ALTER AFTER CREATE)
1678
1692
  // P2P 原生商店 — 卖家本地节点存详情,WebAZ 只锚定 hash + 关键字段
1679
1693
  for (const stmt of [
1680
1694
  'ALTER TABLE products ADD COLUMN p2p_mode INTEGER DEFAULT 0',
@@ -2019,10 +2033,10 @@ try {
2019
2033
  read INTEGER DEFAULT 0,
2020
2034
  created_at TEXT DEFAULT (datetime('now')),
2021
2035
  wish_id TEXT,
2022
- actions TEXT
2036
+ actions TEXT, template_key TEXT, params TEXT
2023
2037
  );
2024
- INSERT INTO notifications_new (id, user_id, order_id, type, title, body, read, created_at, wish_id, actions)
2025
- SELECT id, user_id, order_id, type, title, body, read, created_at, wish_id, actions FROM notifications;
2038
+ INSERT INTO notifications_new (id, user_id, order_id, type, title, body, read, created_at, wish_id, actions, template_key, params)
2039
+ SELECT id, user_id, order_id, type, title, body, read, created_at, wish_id, actions, template_key, params FROM notifications;
2026
2040
  DROP TABLE notifications;
2027
2041
  ALTER TABLE notifications_new RENAME TO notifications;
2028
2042
  CREATE INDEX IF NOT EXISTS idx_notif_user ON notifications(user_id, read, created_at DESC);
@@ -2420,11 +2434,15 @@ try {
2420
2434
  db.exec('CREATE INDEX IF NOT EXISTS idx_auctions_deadline ON auctions(status, deadline_at)');
2421
2435
  }
2422
2436
  catch { }
2423
- // AUC P1:反狙击延长上限
2437
+ // AUC P1:反狙击延长上限;claim_loss_count 从 Sprint 4 数组移来(那里先于本表 CREATE,fresh 库静默失败)
2424
2438
  try {
2425
2439
  db.exec('ALTER TABLE auctions ADD COLUMN max_extends INTEGER DEFAULT 10');
2426
2440
  }
2427
2441
  catch { }
2442
+ try {
2443
+ db.exec('ALTER TABLE auctions ADD COLUMN claim_loss_count INTEGER DEFAULT 0');
2444
+ }
2445
+ catch { }
2428
2446
  try {
2429
2447
  db.exec('ALTER TABLE auctions ADD COLUMN extends_used INTEGER DEFAULT 0');
2430
2448
  }
@@ -4073,8 +4091,8 @@ app.use((req, res, next) => {
4073
4091
  if (req.method === 'GET' && /^\/api\/(info|leaderboard|payment-methods|governance|openapi)/.test(req.path))
4074
4092
  return next();
4075
4093
  const apiKey = req.headers.authorization?.replace('Bearer ', '');
4076
- if (!apiKey)
4077
- return next(); // 无 key 走原 IP 速率
4094
+ if (!apiKey || apiKey.startsWith('gtk_'))
4095
+ return next(); // 无 key→IP 速率;gtk_* 委托凭证(RFC-020)是独立自守 auth 通道(requireAgentGrantScope/resolveActiveGrantByBearer 管 safe-scope+active/expiry/revoked+audit),非 api_key,不进下面 agent 声明/风控/strike;否则合法 safe-scope grant 写(如 POST /api/agent-grants/permission-requests)被误判 AGENT_SCOPE_UNDECLARED。gtk_* 只命中 grant-aware 路由,别处 auth() 自然 401。
4078
4096
  // 2026-05-23 P1 fix:被封禁用户仍可走以下路径(看自己 agent 状态 + 申诉)
4079
4097
  // - GET /api/me/agents 系列(看强 / 申诉前必查)
4080
4098
  // - POST /api/me/agents/strikes/:id/appeal(申诉权不能被封禁阻断)
@@ -4121,15 +4139,15 @@ app.use((req, res, next) => {
4121
4139
  action, cap: capCheck.cap, used: capCheck.used, level: lvlForCap,
4122
4140
  });
4123
4141
  }
4124
- // Phase 3c:风险闸 — 高风险 agent 敏感写降速/暂停(只罚高风险,无责零成本)
4142
+ // Phase 3c:风险闸 — 高风险 agent 敏感写降速/暂停。真人豁免(Tina 案二连,同 #241 原则):绑 Passkey=真人,敏感操作已有 Passkey 仪式把守;风险分含结构罚(同 IP 注册簇)+30 天半衰期,"30 秒重试"对真人=死锁
4125
4143
  const agentRisk = riskInfo.risk;
4126
- if (agentRisk >= 100) {
4144
+ if (agentRisk >= 100 && !riskInfo.hasPasskey) {
4127
4145
  return void res.status(403).json({
4128
4146
  error: 'agent 风险分已达上限,敏感操作已暂停 — 请在「我的 Agents」查看并申诉',
4129
4147
  error_code: 'AGENT_RISK_SUSPENDED', action, risk: agentRisk,
4130
4148
  });
4131
4149
  }
4132
- if (agentRisk >= 70) {
4150
+ if (agentRisk >= 70 && !riskInfo.hasPasskey) {
4133
4151
  res.setHeader('Retry-After', '30');
4134
4152
  return void res.status(429).json({
4135
4153
  error: `agent 风险分偏高(${agentRisk}/100),敏感操作已降速,请 30 秒后重试`,
@@ -4169,7 +4187,8 @@ app.use((req, res, next) => {
4169
4187
  }
4170
4188
  }
4171
4189
  }
4172
- // 分档 rate limit
4190
+ if (riskInfo.hasPasskey || req.path === '/api/signaling/poll' || req.path === '/api/snf/pending')
4191
+ return next(); // 分档 rate limit 前的真人豁免(Tina 案修复):绑 Passkey=真人本人/监护(PWA 页面加载+轮询天然高频,agent 约束原则=真人豁免,不进速率桶不触发 rate_limit_abuse strike);高频轮询端点亦不计 agent 配额
4173
4192
  const repRow = db.prepare(`SELECT level FROM agent_reputation WHERE api_key = ?`).get(apiKey);
4174
4193
  const level = repRow?.level || 'new';
4175
4194
  const cap = getAgentRateCap(level);
@@ -4475,9 +4494,12 @@ registerAgentReputationRoutes(app, {
4475
4494
  registerUsersPublicRoutes(app, { db, auth, noteAuthenticityBadges });
4476
4495
  registerDirectPayDisclosureAckRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4d
4477
4496
  registerDirectReceivePaymentInstructionRoutes(app, { db, auth, generateId });
4478
- registerDirectReceiveAccountsRoutes(app, { db, auth, generateId, consumeGateToken }); // PR-4f-a + Phase C1
4479
- registerDirectPayAvailabilityRoutes(app, { db, auth, getProtocolParam, generateId }); // PR-4a + 缓交 apply/status
4480
- registerAdminDirectReceiveDepositsRoutes(app, { db, requireRootAdmin: (req, res) => requireRootAdmin(req, res), consumeGateToken, logAdminAction, getProtocolParam }); // PR-4b-3 + readiness
4497
+ registerDirectReceiveAccountsRoutes(app, { db, auth, generateId, consumeGateToken });
4498
+ registerFeePrepayRequestRoutes(app, { db, auth, generateId }); // PR-4f-a + Phase C1 + fee 预充值申请
4499
+ registerDirectPayAvailabilityRoutes(app, { db, auth, getProtocolParam, generateId });
4500
+ registerSellerDirectPayReportRoutes(app, { db, auth }); // PR-4a + 缓交 + 卖家直付销售统计/对账/平台费明细(只读)
4501
+ registerAdminDirectReceiveDepositsRoutes(app, { db, requireRootAdmin: (req, res) => requireRootAdmin(req, res), consumeGateToken, logAdminAction, getProtocolParam });
4502
+ registerPlatformReceiveAccountsRoutes(app, { db, requireRootAdmin: (req, res) => requireRootAdmin(req, res), generateId, consumeGateToken, logAdminAction }); // PR-4b-3 + readiness + 平台多收款方式 admin CRUD(root+Passkey)
4481
4503
  // RFC-004 build_feedback — agent-native "use → build" 反馈管道
4482
4504
  registerBuildFeedbackRoutes(app, {
4483
4505
  db, auth,
@@ -5845,25 +5867,20 @@ function getArbitratorState(userId) {
5845
5867
  state = 'rejected';
5846
5868
  if (wl)
5847
5869
  state = 'approved';
5848
- return { state, application: app ?? null, whitelist: wl ?? null };
5870
+ // PR-E:真实运行时能力字段。前端必须以 can_arbitrate 为准(state='approved' 不区分 suspended/revoked)。
5871
+ const arbitrator_status = wl ? (wl.status ?? 'active') : 'none'; // active / suspended / revoked / none
5872
+ return { state, can_arbitrate: isEligibleArbitrator(userId).ok, arbitrator_status, application: app ?? null, whitelist: wl ?? null };
5849
5873
  }
5850
- // 仲裁员身份判定(内部 role + 外部 whitelist 双通道)— 用于 /api/disputes/:id/arbitrate 守门
5874
+ // 仲裁员身份判定 PR-B:唯一授权源 = active arbitrator_whitelist(role='arbitrator' 旁路已移除)。域逻辑见 arbitrator-lifecycle.ts。
5851
5875
  function isEligibleArbitrator(userId) {
5852
- const u = db.prepare("SELECT role FROM users WHERE id = ?").get(userId);
5853
- if (!u)
5854
- return { ok: false, reason: '用户不存在' };
5855
- if (u.role === 'arbitrator')
5856
- return { ok: true, via: 'role' };
5857
- const wl = db.prepare("SELECT user_id FROM arbitrator_whitelist WHERE user_id = ?").get(userId);
5858
- if (wl)
5859
- return { ok: true, via: 'whitelist' };
5860
- return { ok: false, reason: '非仲裁员 — 需 role=arbitrator(内部)或 arbitrator_whitelist(外部)' };
5876
+ return isEligibleArbitratorImpl(db, userId);
5861
5877
  }
5862
5878
  // #1013 Phase 44: 4 user + 3 admin arbitrator endpoints 已迁出到 routes/arbitrator.ts
5863
5879
  registerArbitratorRoutes(app, {
5864
5880
  db, generateId, auth,
5865
5881
  requireArbitrationAdmin: (req, res) => requireAdminPermission(req, res, 'arbitration'),
5866
5882
  checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction,
5883
+ consumeGateToken, // PR-B: grant/suspend/reinstate/revoke 的 purpose-bound 真人 Passkey
5867
5884
  ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS,
5868
5885
  });
5869
5886
  // Governance onboarding (W3.5-B #1093) — apply + quiz + cases + admin activation + resign/appeal + auto_deactivate audit
@@ -6061,7 +6078,7 @@ registerSellerQuotaRoutes(app, {
6061
6078
  registerDisputesReadRoutes(app, {
6062
6079
  db, auth, errorRes,
6063
6080
  getOpenDisputes, getDisputeDetails, getEvidenceRequests, listEvidenceFiles,
6064
- isEligibleArbitrator,
6081
+ isEligibleArbitrator, isArbitrationAdmin: isArbitrationReadAdmin, // 后者=争议详情只读 admin 门,与后台列表 /admin/disputes 同款授权(单一真相源 arbitration-read-admin.ts)
6065
6082
  });
6066
6083
  // #1013 Phase 87: 5 disputes 写端点已迁出(包括 234 行 arbitrate)
6067
6084
  // FUND_BASE_RATE 是 const 函数在文件下游定义;用 getter 包一层避免 TDZ
@@ -6074,8 +6091,14 @@ registerDisputesWriteRoutes(app, {
6074
6091
  FUND_BASE_RATE: () => FUND_BASE_RATE(),
6075
6092
  settleCommission, depositToFund, calculatePv,
6076
6093
  recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend,
6077
- getProtocolParam,
6094
+ getProtocolParam, notifyTransition,
6078
6095
  });
6096
+ registerMutualCancelRoutes(app, { db, auth, generateId, errorRes });
6097
+ registerDirectPayCancelRefundRoutes(app, { db, auth, generateId, errorRes, consumeGateToken });
6098
+ registerDirectPayReturnsRoutes(app, { db, auth, generateId, errorRes, consumeGateToken });
6099
+ registerDirectPayPendingAcceptRoutes(app, { db, auth, errorRes, getProtocolParam });
6100
+ registerShippingTemplateRoutes(app, { db, auth, errorRes });
6101
+ registerBondSellerRoutes(app, { db, auth, generateId, errorRes, getProtocolParam }); // 协商取消(disputed)+ 直付取消退款握手(accepted):关单手术均内部 db.transaction 原子
6079
6102
  // lightAuthGuard:轻量 Authorization 头守门(在 raw 解析之前挡掉无 auth 请求)
6080
6103
  // 被 Phase 13 shareables(视频上传)+ Phase 87 disputes evidence-blob 共享
6081
6104
  function lightAuthGuard(req, res, next) {
@@ -7847,7 +7870,7 @@ registerWebauthnRoutes(app, {
7847
7870
  origin: WEBAUTHN_ORIGIN,
7848
7871
  challengeTtlMs: WEBAUTHN_CHALLENGE_TTL_MS,
7849
7872
  gateTtlMs: WEBAUTHN_GATE_TTL_MS,
7850
- invalidateAgentRiskCacheForUser,
7873
+ invalidateAgentRiskCacheForUser, createProductDraftHandler: makeCreateProductHandler({ db, auth, generateId, checkSellerCanList, getStakeDiscount, VALID_PRODUCT_TYPES, parsePlatformUrl, makeCommitmentHash, makeDescriptionHash, makePriceHash }), // RFC-020 PR-4: grant-gated warehouse draft reuses the human create logic
7851
7874
  requireHumanPresence, // #1044 — DELETE passkey 自身需 token
7852
7875
  });
7853
7876
  // consumeGateToken / requireHumanPresence 已抽出到 ./human-presence.ts(PR-F0,behavior-zero,
@@ -27,6 +27,13 @@ export const SAFE_SCOPES = [
27
27
  'list_product_draft',
28
28
  'product_publish_request',
29
29
  'draft_order',
30
+ // Seller-scoped read/draft surfaces (Catalog Agent role) — read the seller's own catalog + propose
31
+ // drafts/pricing/publish-REQUESTS. NONE of these publish, accept orders, ship, or move money.
32
+ 'seller_profile_read',
33
+ 'seller_products_read',
34
+ 'seller_inventory_read',
35
+ 'seller_product_draft',
36
+ 'seller_pricing_suggestion',
30
37
  ];
31
38
  /**
32
39
  * Real actions that WILL require a live Passkey each time — but are NOT yet gated
@@ -119,10 +126,71 @@ export function grantIsActive(grant, nowIso) {
119
126
  }
120
127
  /** Short-lived bearer policy for SAFE scopes (RFC-020 bearer-first; PoP before risk/longer). */
121
128
  export const GRANT_TTL_DEFAULT_SEC = 3600; // 1h
122
- export const GRANT_TTL_MAX_SEC = 24 * 3600; // 24h cap — short-lived only
129
+ export const GRANT_TTL_MAX_SEC = 24 * 3600; // 24h cap — short-lived only (legacy webaz_pair bearer)
123
130
  export function clampTtlSeconds(requested) {
124
131
  const n = Number(requested);
125
132
  if (!Number.isFinite(n) || n <= 0)
126
133
  return GRANT_TTL_DEFAULT_SEC;
127
134
  return Math.min(Math.floor(n), GRANT_TTL_MAX_SEC);
128
135
  }
136
+ export const PERMISSION_BUNDLES = {
137
+ catalog_agent: {
138
+ key: 'catalog_agent',
139
+ label: 'Catalog Agent',
140
+ scopes: ['read_public', 'profile_read', 'search', 'seller_profile_read', 'seller_products_read', 'seller_inventory_read', 'seller_product_draft', 'seller_pricing_suggestion', 'product_publish_request'],
141
+ human_summary: '选品、商品草稿、库存字段检查、价格建议、上架请求。它不能发布商品、不能接单、不能发货、不能动用资金。',
142
+ human_summary_en: 'Sourcing, product drafts, inventory-field checks, pricing suggestions, and publish REQUESTS. It cannot publish products, accept orders, ship, or move funds.',
143
+ },
144
+ };
145
+ /** Resolve a bundle key → its safe scopes + human summary, or null if unknown. */
146
+ export function resolveBundle(key) {
147
+ return (typeof key === 'string' && Object.prototype.hasOwnProperty.call(PERMISSION_BUNDLES, key)) ? PERMISSION_BUNDLES[key] : null;
148
+ }
149
+ /** INVARIANT check (acceptance #7): a bundle must be all-safe. Returns the offending non-safe scopes. */
150
+ export function bundleNonSafeScopes(bundle) {
151
+ return bundle.scopes.filter(s => classifyScope(s) !== 'safe');
152
+ }
153
+ // Load-time assertion: no bundle may ship with a risk/never/unknown scope (fail LOUD at import, not runtime).
154
+ for (const b of Object.values(PERMISSION_BUNDLES)) {
155
+ const bad = bundleNonSafeScopes(b);
156
+ if (bad.length)
157
+ throw new Error(`Permission bundle '${b.key}' contains non-safe scope(s): ${bad.join(', ')} — bundles are safe-only (RFC-020)`);
158
+ }
159
+ export const DURATION_SECONDS = { '1h': 3600, '24h': 86400, '7d': 604800, '30d': 2592000 };
160
+ /**
161
+ * Which durations a set of scopes may be granted for, capped by the HIGHEST risk tier present:
162
+ * - never-delegable / unknown → [] (cannot be granted at all)
163
+ * - risk (high) → ['once'] (single Passkey each time; NEVER long-term)
164
+ * - safe (low/medium) → once…30d (long-term ok)
165
+ */
166
+ export function allowedDurationsForScopes(scopes) {
167
+ const tiers = scopes.map(classifyScope);
168
+ if (tiers.some(t => t === 'never_delegable' || t === 'unknown'))
169
+ return [];
170
+ if (tiers.some(t => t === 'risk'))
171
+ return ['once'];
172
+ return ['once', '1h', '24h', '7d', '30d'];
173
+ }
174
+ export function durationAllowedForScopes(scopes, duration) {
175
+ return typeof duration === 'string' && allowedDurationsForScopes(scopes).includes(duration);
176
+ }
177
+ /** Suggested default: 7d for long-term-eligible safe scopes; else the safest available (once). */
178
+ export function suggestedDurationForScopes(scopes) {
179
+ const a = allowedDurationsForScopes(scopes);
180
+ return a.includes('7d') ? '7d' : (a[0] ?? 'once');
181
+ }
182
+ /** Grant lifetime in seconds for an approved (non-'once') duration. 'once' handled separately (single-use). */
183
+ export function durationToSeconds(duration) {
184
+ return duration === 'once' ? 0 : DURATION_SECONDS[duration];
185
+ }
186
+ /** Coarse risk label for the human-facing request card. */
187
+ export function riskLevelForScopes(scopes) {
188
+ const tiers = scopes.map(classifyScope);
189
+ if (tiers.some(t => t === 'never_delegable' || t === 'unknown'))
190
+ return 'blocked';
191
+ if (tiers.some(t => t === 'risk'))
192
+ return 'high';
193
+ if (scopes.includes('product_publish_request'))
194
+ return 'medium'; // request-only (still human-gated to publish), flag as medium
195
+ return 'low';
196
+ }
@@ -784,6 +784,17 @@ export function initArbitratorReviewSchema(db) {
784
784
  stake_amount INTEGER DEFAULT 0
785
785
  )
786
786
  `);
787
+ // PR-B 生产仲裁员生命周期字段(ADD COLUMN 必在 CREATE 后 —— schema 铁律)。legacy 行 status NULL 视为 active。
788
+ for (const alter of [
789
+ `ALTER TABLE arbitrator_whitelist ADD COLUMN status TEXT DEFAULT 'active'`, // active | suspended | revoked
790
+ `ALTER TABLE arbitrator_whitelist ADD COLUMN suspended_at TEXT`,
791
+ `ALTER TABLE arbitrator_whitelist ADD COLUMN revoked_at TEXT`,
792
+ ]) {
793
+ try {
794
+ db.exec(alter);
795
+ }
796
+ catch { /* 列已存在 */ }
797
+ }
787
798
  try {
788
799
  db.exec('CREATE INDEX IF NOT EXISTS idx_arb_apps_status ON arbitrator_applications(status)');
789
800
  }
@@ -1220,7 +1231,7 @@ export function initReturnRequestsSchema(db) {
1220
1231
  reason TEXT NOT NULL, -- 'quality' | 'wrong_item' | 'damaged' | 'no_longer_needed' | 'other'
1221
1232
  reason_text TEXT,
1222
1233
  refund_amount DECIMAL(18,2), -- 默认 = order.total_amount
1223
- status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | refunded | escalated | cancelled
1234
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | refunded | escalated | cancelled | (direct_p2p) await_refund | refund_marked
1224
1235
  seller_response TEXT,
1225
1236
  escalated_dispute_id TEXT,
1226
1237
  created_at TEXT DEFAULT (datetime('now')),
@@ -1239,6 +1250,15 @@ export function initReturnRequestsSchema(db) {
1239
1250
  db.exec('CREATE INDEX IF NOT EXISTS idx_returns_order ON return_requests(order_id)');
1240
1251
  }
1241
1252
  catch { }
1253
+ // 直付(direct_p2p)送达后退货·场外退款握手(src/direct-pay-returns.ts)。ALTER 必须在 CREATE 之后(fresh DB silent-fail 铁律)。
1254
+ try {
1255
+ db.exec('ALTER TABLE return_requests ADD COLUMN refund_reference TEXT');
1256
+ }
1257
+ catch { /* 已存在 */ }
1258
+ try {
1259
+ db.exec('ALTER TABLE return_requests ADD COLUMN await_refund_since TEXT');
1260
+ }
1261
+ catch { /* 已存在 */ }
1242
1262
  }
1243
1263
  // ─── W2 售后协商时间线(flagged/flag_reasons ALTER 刻意留 server.ts 原位)──
1244
1264
  export function initReturnMessagesSchema(db) {
@@ -1784,13 +1804,47 @@ export function initAgentDelegationGrantsSchema(db) {
1784
1804
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1785
1805
  expires_at TEXT NOT NULL, -- short-lived (clamped, RFC-020 bearer-first)
1786
1806
  revoked_at TEXT,
1787
- revoked_reason TEXT
1807
+ revoked_reason TEXT,
1808
+ permission_bundle TEXT -- named bundle key if issued/expanded via a bundle (e.g. 'catalog_agent'); NULL = ad-hoc scopes
1788
1809
  )
1789
1810
  `);
1811
+ try {
1812
+ db.exec(`ALTER TABLE agent_delegation_grants ADD COLUMN permission_bundle TEXT`);
1813
+ }
1814
+ catch { /* existing DB: column already added */ }
1790
1815
  db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_human ON agent_delegation_grants(human_id, status)`);
1791
1816
  db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_token ON agent_delegation_grants(token_hash)`);
1792
1817
  db.exec(`CREATE INDEX IF NOT EXISTS idx_adg_expiry ON agent_delegation_grants(status, expires_at)`);
1793
1818
  }
1819
+ /**
1820
+ * RFC-020 — Agent Permission Requests (JIT + bundle authorization). An already-connected agent (holds a
1821
+ * grant) that hits a missing scope, or wants a job bundle, lodges a REQUEST here; the human sees it in
1822
+ * #agent-approvals and approves/rejects. Approval expands the agent's existing grant (safe scopes only,
1823
+ * duration-capped). Bound to (human_id, grant_id). NO money/order/status columns; a request grants nothing
1824
+ * until a human approves it.
1825
+ */
1826
+ export function initAgentPermissionRequestsSchema(db) {
1827
+ db.exec(`
1828
+ CREATE TABLE IF NOT EXISTS agent_permission_requests (
1829
+ id TEXT PRIMARY KEY, -- apr_xxx
1830
+ human_id TEXT NOT NULL, -- resolved from the requesting agent's grant
1831
+ grant_id TEXT NOT NULL, -- the agent's existing grant that approval expands
1832
+ agent_label TEXT,
1833
+ requested_scopes TEXT NOT NULL DEFAULT '[]', -- JSON string[] — resolved (bundle expanded) safe scopes
1834
+ permission_bundle TEXT, -- bundle key if requested as a bundle; NULL = ad-hoc
1835
+ reason TEXT, -- agent free-text (display only, unverified)
1836
+ task_context TEXT, -- agent free-text about the task (display only)
1837
+ risk_level TEXT NOT NULL DEFAULT 'low', -- low | medium | high (derived, not agent-supplied)
1838
+ duration TEXT NOT NULL DEFAULT '7d', -- once | 1h | 24h | 7d | 30d (capped by risk tier)
1839
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | rejected | expired | revoked
1840
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1841
+ expires_at TEXT NOT NULL, -- request TTL (auto-expire if unanswered)
1842
+ approved_at TEXT
1843
+ )
1844
+ `);
1845
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_apr_human ON agent_permission_requests(human_id, status)`);
1846
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_apr_status ON agent_permission_requests(status, expires_at)`);
1847
+ }
1794
1848
  /**
1795
1849
  * RFC-020 PR-C1 — agent pairing sessions (OAuth device-flow + PKCE shape).
1796
1850
  *
@@ -1812,7 +1866,7 @@ export function initAgentPairingSchema(db) {
1812
1866
  agent_pubkey TEXT, -- RESERVED (PoP); stored if sent, NOT verified in C1
1813
1867
  reason TEXT, -- agent free-text reason (shown in consent)
1814
1868
  capabilities TEXT NOT NULL DEFAULT '[]', -- requested SAFE scopes (validated safe-only at start)
1815
- status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | consumed | expired | revoked
1869
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | consumed | expired | revoked | rejected(human declined)
1816
1870
  human_id TEXT, -- set on approve
1817
1871
  grant_id TEXT, -- set on approve (issued grant; token_hash filled at retrieve)
1818
1872
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
@@ -0,0 +1,116 @@
1
+ const REGION_RE = /^[A-Z0-9-]{2,8}$/;
2
+ /** parse-don't-validate:坏 JSON/坏形状 → null(= 不限,fail-open 到原行为;写入侧另有严校验)。 */
3
+ export function parseSaleRegionsRule(raw) {
4
+ if (typeof raw !== 'string' || !raw)
5
+ return null;
6
+ try {
7
+ const r = JSON.parse(raw);
8
+ if (!r || (r.mode !== 'all' && r.mode !== 'list'))
9
+ return null;
10
+ const norm = (a) => Array.isArray(a) ? a.map(x => String(x).toUpperCase()).filter(x => REGION_RE.test(x)) : undefined;
11
+ return { mode: r.mode, include: norm(r.include), exclude: norm(r.exclude) };
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ /** 写入侧严校验:返回规范化 JSON 串,或 {error}。null/空 = 清除(继承上层)。 */
18
+ export function validateSaleRegionsInput(raw) {
19
+ if (raw === null || raw === undefined || raw === '')
20
+ return { json: null };
21
+ const r = raw;
22
+ if (typeof r !== 'object' || (r.mode !== 'all' && r.mode !== 'list'))
23
+ return { error: "sale_regions.mode 必须是 'all' 或 'list'" };
24
+ const norm = (a, name) => {
25
+ if (a === undefined || a === null)
26
+ return undefined;
27
+ if (!Array.isArray(a) || a.length > 64)
28
+ return { error: `sale_regions.${name} 须为 ≤64 项数组` };
29
+ const out = [];
30
+ for (const x of a) {
31
+ const code = String(x).trim().toUpperCase();
32
+ if (!REGION_RE.test(code))
33
+ return { error: `sale_regions.${name} 含非法地区码:${String(x).slice(0, 12)}(2-8 位大写字母/数字/-)` };
34
+ out.push(code);
35
+ }
36
+ return [...new Set(out)];
37
+ };
38
+ const inc = norm(r.include, 'include');
39
+ if (inc && 'error' in inc)
40
+ return inc;
41
+ const exc = norm(r.exclude, 'exclude');
42
+ if (exc && 'error' in exc)
43
+ return exc;
44
+ if (r.mode === 'list' && (!inc || inc.length === 0))
45
+ return { error: "mode='list' 时 include 不能为空(否则等于全不可卖 —— 想全不可卖请直接下架)" };
46
+ return { json: JSON.stringify({ mode: r.mode, ...(inc ? { include: inc } : {}), ...(exc ? { exclude: exc } : {}) }) };
47
+ }
48
+ /** 生效规则:商品 ?? 店铺(与 accept_mode/运费模板同层级约定)。 */
49
+ export function effectiveSaleRegionsRule(db, product, sellerId) {
50
+ const own = parseSaleRegionsRule(product.sale_regions);
51
+ if (own)
52
+ return own;
53
+ try {
54
+ const row = db.prepare('SELECT store_sale_regions FROM users WHERE id = ?').get(sellerId);
55
+ return parseSaleRegionsRule(row?.store_sale_regions);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * 平台合规名单解析(单一真相源:建单门 gateSaleRegionForCreate + S5 预检 shipping-options 共用)。
63
+ * 坏配置(非 JSON / 非数组)→ { ok:false },由调用方 fail-closed(建单 503;预检 sellable=platform_policy_invalid)。
64
+ * 缺省 '[]' → { ok:true, list:[] }。区码统一大写。
65
+ */
66
+ export function parsePlatformBlocklist(raw) {
67
+ try {
68
+ const p = JSON.parse(String(raw ?? '[]'));
69
+ if (!Array.isArray(p))
70
+ return { ok: false };
71
+ return { ok: true, list: p.map(x => String(x).toUpperCase()) };
72
+ }
73
+ catch {
74
+ return { ok: false };
75
+ }
76
+ }
77
+ export function regionAllowedByRule(rule, region) {
78
+ const r = region.toUpperCase();
79
+ if (rule.exclude?.includes(r))
80
+ return false;
81
+ if (rule.mode === 'list')
82
+ return !!rule.include?.includes(r);
83
+ return true;
84
+ }
85
+ /**
86
+ * 建单可售门(orders-create 单行调用,两轨共用,运费 gate 之前、任何写之前)。
87
+ * 返回 false = 已写错误响应。平台 overlay 先裁(商家规则不可放宽合规);规则存在但买家没给地区 → fail-closed 要求补选。
88
+ */
89
+ export function gateSaleRegionForCreate(db, res, product, sellerId, rawRegion, getProtocolParam) {
90
+ const region = (typeof rawRegion === 'string' && rawRegion.trim()) ? rawRegion.trim().toUpperCase() : null;
91
+ // ① 平台合规 overlay(protocol param,DEFAULT_PARAMS 已 seed '[]';admin PATCH 有 JSON+区码校验)。
92
+ // 坏配置【fail-closed】:合规名单读不懂时放行 = 静默解除平台禁售(审计 P2) —— 宁可挡单也不裸奔;
93
+ // admin 写入侧已强校验,坏值只可能来自手改 DB,错误信息直接指路。
94
+ const parsedBlock = parsePlatformBlocklist(getProtocolParam('trade.platform_region_blocklist', '[]'));
95
+ if (!parsedBlock.ok) {
96
+ res.status(503).json({ error: '平台合规配置异常,暂无法下单(运营侧需修复 trade.platform_region_blocklist 参数)', error_code: 'PLATFORM_REGION_POLICY_INVALID' });
97
+ return false;
98
+ }
99
+ const platformBlock = parsedBlock.list;
100
+ const rule = effectiveSaleRegionsRule(db, product, sellerId);
101
+ if (platformBlock.length === 0 && !rule)
102
+ return true; // 无任何限制 = 原行为(地区可选)
103
+ if (!region) {
104
+ res.status(400).json({ error: '该商品设有可售地区限制,请选择收货国家/地区', error_code: 'SHIP_REGION_REQUIRED' });
105
+ return false;
106
+ }
107
+ if (platformBlock.includes(region)) {
108
+ res.status(409).json({ error: '平台合规限制:该商品暂不支持销售到该地区', error_code: 'PRODUCT_RESTRICTED', region });
109
+ return false;
110
+ }
111
+ if (rule && !regionAllowedByRule(rule, region)) {
112
+ res.status(409).json({ error: '卖家未将该地区设为可售范围(与运费无关,不适用询价)', error_code: 'REGION_NOT_FOR_SALE', region });
113
+ return false;
114
+ }
115
+ return true;
116
+ }