@seasonkoh/webaz 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/direct-pay-bond-rail-clearance.js +10 -5
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-create.js +78 -9
- package/dist/direct-pay-disclosures.js +17 -11
- package/dist/direct-pay-fee-ar.js +2 -2
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-deferral.js +30 -1
- package/dist/direct-receive-deposits.js +122 -17
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +7 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +81 -16
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- package/dist/platform-receive-accounts.js +94 -0
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +15 -0
- package/dist/pwa/direct-pay-order-redaction.js +20 -2
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +6 -6
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
- package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
- package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay.js +36 -37
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- package/dist/pwa/public/app-profile.js +4 -4
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +1 -1
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +133 -160
- package/dist/pwa/public/i18n.js +675 -5
- package/dist/pwa/public/index.html +41 -0
- package/dist/pwa/public/openapi.json +709 -11
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -0
- package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
- package/dist/pwa/routes/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/logistics.js +4 -4
- package/dist/pwa/routes/me-data.js +2 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +182 -9
- package/dist/pwa/routes/orders-create.js +18 -7
- package/dist/pwa/routes/orders-read.js +76 -14
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +13 -4
- package/dist/pwa/routes/products-update.js +15 -1
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/returns.js +39 -8
- package/dist/pwa/routes/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/snf.js +4 -1
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +59 -36
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +57 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +59 -2
|
@@ -45,7 +45,7 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
45
45
|
const d30 = fmt(new Date(now - 30 * 86400000));
|
|
46
46
|
const d60 = fmt(new Date(now - 60 * 86400000));
|
|
47
47
|
const orders = await dbAll(`
|
|
48
|
-
SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at,
|
|
48
|
+
SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at, o.payment_rail,
|
|
49
49
|
COALESCE(p.title, '已下架商品') as product_title,
|
|
50
50
|
COALESCE(ub.name, '匿名') as buyer_name
|
|
51
51
|
FROM orders o
|
|
@@ -60,6 +60,8 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
60
60
|
const inLast30 = orders.filter(o => o.created_at >= d30);
|
|
61
61
|
const inPrev30 = orders.filter(o => o.created_at < d30);
|
|
62
62
|
const gmv = (arr) => arr.filter(o => completedStatuses.has(o.status)).reduce((s, o) => s + Number(o.total_amount || 0), 0);
|
|
63
|
+
// GMV 按支付轨拆分:托管=平台真实托管收入,直接收款=场外收款(平台不经手)—— 不再混算(诚实口径)
|
|
64
|
+
const gmvRail = (arr, rail) => arr.filter(o => completedStatuses.has(o.status) && (rail === 'direct_p2p' ? o.payment_rail === 'direct_p2p' : (o.payment_rail || 'escrow') === 'escrow')).reduce((s, o) => s + Number(o.total_amount || 0), 0);
|
|
63
65
|
const curGmv = gmv(inLast30);
|
|
64
66
|
const prevGmv = gmv(inPrev30);
|
|
65
67
|
const curCount = inLast30.length;
|
|
@@ -116,6 +118,8 @@ export function registerSellerQuotaRoutes(app, deps) {
|
|
|
116
118
|
period_days: 30,
|
|
117
119
|
summary: {
|
|
118
120
|
gmv: curGmv,
|
|
121
|
+
gmv_escrow: gmvRail(inLast30, 'escrow'),
|
|
122
|
+
gmv_direct_pay: gmvRail(inLast30, 'direct_p2p'),
|
|
119
123
|
order_count: curCount,
|
|
120
124
|
completed_count: completedOrders30.length,
|
|
121
125
|
aov,
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { parseShippingTemplate, loadTemplateJson, resolveShipping } from '../../shipping-templates.js';
|
|
2
|
+
import { validateSaleRegionsInput, parseSaleRegionsRule, regionAllowedByRule, parsePlatformBlocklist } from '../../sale-regions.js'; // S1 可售区域(店铺+单品写入;gate 在 orders-create;S5 只读披露复用判定 + 与建单门同一 blocklist parser)
|
|
3
|
+
import { validateFreeShippingThreshold } from '../../free-shipping.js'; // 营销域满额免邮(S2 返工:写入口暂列本设置面,规则/判定在 free-shipping.ts)
|
|
4
|
+
import { validateImportDutyTerms, validateTaxLines, effectiveImportDutyTerms, effectiveTaxLines, parseTaxLines, taxLinesForRegion } from '../../trade-tax.js'; // S3 跨境税费/进口责任声明层(seller-declared,平台不算不收)
|
|
5
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
6
|
+
export function registerShippingTemplateRoutes(app, deps) {
|
|
7
|
+
const { auth, errorRes } = deps;
|
|
8
|
+
app.post('/api/seller/shipping-template', async (req, res) => {
|
|
9
|
+
const user = auth(req, res);
|
|
10
|
+
if (!user)
|
|
11
|
+
return;
|
|
12
|
+
if (user.role !== 'seller')
|
|
13
|
+
return void errorRes(res, 403, 'SELLER_ONLY', '仅卖家可设置运费模板');
|
|
14
|
+
const b = req.body || {};
|
|
15
|
+
const touched = {};
|
|
16
|
+
if ('store_template' in b) {
|
|
17
|
+
const p = parseShippingTemplate(b.store_template);
|
|
18
|
+
if (!p.ok)
|
|
19
|
+
return void errorRes(res, 400, 'BAD_SHIPPING_TEMPLATE', p.error);
|
|
20
|
+
await dbRun('UPDATE users SET store_shipping_template = ? WHERE id = ?', [p.entries ? JSON.stringify(p.entries) : null, user.id]);
|
|
21
|
+
touched.store_template = p.entries;
|
|
22
|
+
}
|
|
23
|
+
if ('store_quote_ok' in b) { // PR-3:模板外地区询价 opt-in(店铺默认;直付轨生效)
|
|
24
|
+
if (b.store_quote_ok !== null && typeof b.store_quote_ok !== 'boolean')
|
|
25
|
+
return void errorRes(res, 400, 'BAD_QUOTE_OK', 'store_quote_ok 只允许 true|false|null');
|
|
26
|
+
await dbRun('UPDATE users SET store_shipping_quote_ok = ? WHERE id = ?', [b.store_quote_ok === null ? null : (b.store_quote_ok ? 1 : 0), user.id]);
|
|
27
|
+
touched.store_quote_ok = b.store_quote_ok;
|
|
28
|
+
}
|
|
29
|
+
if ('store_sale_regions' in b) { // S1 可售区域(店铺默认):{mode:'all'|'list', include?, exclude?};null=清除(不限)
|
|
30
|
+
const v = validateSaleRegionsInput(b.store_sale_regions);
|
|
31
|
+
if ('error' in v)
|
|
32
|
+
return void errorRes(res, 400, 'BAD_SALE_REGIONS', v.error);
|
|
33
|
+
await dbRun('UPDATE users SET store_sale_regions = ? WHERE id = ?', [v.json, user.id]);
|
|
34
|
+
touched.store_sale_regions = v.json ? JSON.parse(v.json) : null;
|
|
35
|
+
}
|
|
36
|
+
if ('product_id' in b && 'sale_regions' in b) { // S1 单品覆盖(优先于店铺;null=回落店铺)
|
|
37
|
+
const v = validateSaleRegionsInput(b.sale_regions);
|
|
38
|
+
if ('error' in v)
|
|
39
|
+
return void errorRes(res, 400, 'BAD_SALE_REGIONS', v.error);
|
|
40
|
+
const prodS = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
41
|
+
if (!prodS)
|
|
42
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
43
|
+
if (prodS.seller_id !== user.id)
|
|
44
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
|
|
45
|
+
await dbRun('UPDATE products SET sale_regions = ? WHERE id = ?', [v.json, b.product_id]);
|
|
46
|
+
touched.product_sale_regions = v.json ? JSON.parse(v.json) : null;
|
|
47
|
+
}
|
|
48
|
+
if ('store_free_shipping_threshold' in b) { // 营销:满额免邮(店铺默认;null=清除)。券后货款≥阈值→运费商家承担
|
|
49
|
+
const v = validateFreeShippingThreshold(b.store_free_shipping_threshold);
|
|
50
|
+
if ('error' in v)
|
|
51
|
+
return void errorRes(res, 400, 'BAD_FREE_SHIPPING_THRESHOLD', v.error);
|
|
52
|
+
await dbRun('UPDATE users SET store_free_shipping_threshold = ? WHERE id = ?', [v.value, user.id]);
|
|
53
|
+
touched.store_free_shipping_threshold = v.value;
|
|
54
|
+
}
|
|
55
|
+
if ('product_id' in b && 'free_shipping_threshold' in b) { // 营销:单品覆盖(null=回落店铺)
|
|
56
|
+
const v = validateFreeShippingThreshold(b.free_shipping_threshold);
|
|
57
|
+
if ('error' in v)
|
|
58
|
+
return void errorRes(res, 400, 'BAD_FREE_SHIPPING_THRESHOLD', v.error);
|
|
59
|
+
const prodF = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
60
|
+
if (!prodF)
|
|
61
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
62
|
+
if (prodF.seller_id !== user.id)
|
|
63
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
|
|
64
|
+
await dbRun('UPDATE products SET free_shipping_threshold = ? WHERE id = ?', [v.value, b.product_id]);
|
|
65
|
+
touched.product_free_shipping_threshold = v.value;
|
|
66
|
+
}
|
|
67
|
+
if ('store_import_duty_terms' in b) { // S3:DDP/DDU 进口责任声明(店铺默认;null=清除)
|
|
68
|
+
const v = validateImportDutyTerms(b.store_import_duty_terms);
|
|
69
|
+
if ('error' in v)
|
|
70
|
+
return void errorRes(res, 400, 'BAD_IMPORT_DUTY_TERMS', v.error);
|
|
71
|
+
await dbRun('UPDATE users SET store_import_duty_terms = ? WHERE id = ?', [v.value, user.id]);
|
|
72
|
+
touched.store_import_duty_terms = v.value;
|
|
73
|
+
}
|
|
74
|
+
if ('product_id' in b && 'import_duty_terms' in b) { // S3:单品覆盖(null=回落店铺)
|
|
75
|
+
const v = validateImportDutyTerms(b.import_duty_terms);
|
|
76
|
+
if ('error' in v)
|
|
77
|
+
return void errorRes(res, 400, 'BAD_IMPORT_DUTY_TERMS', v.error);
|
|
78
|
+
const prodD = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
79
|
+
if (!prodD)
|
|
80
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
81
|
+
if (prodD.seller_id !== user.id)
|
|
82
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
|
|
83
|
+
await dbRun('UPDATE products SET import_duty_terms = ? WHERE id = ?', [v.value, b.product_id]);
|
|
84
|
+
touched.product_import_duty_terms = v.value;
|
|
85
|
+
}
|
|
86
|
+
if ('store_tax_lines' in b) { // S3:价内已含税声明(店铺默认;仅 'included';null=清除)
|
|
87
|
+
const v = validateTaxLines(b.store_tax_lines);
|
|
88
|
+
if ('error' in v)
|
|
89
|
+
return void errorRes(res, 400, 'BAD_TAX_LINES', v.error);
|
|
90
|
+
await dbRun('UPDATE users SET store_tax_lines = ? WHERE id = ?', [v.value, user.id]);
|
|
91
|
+
touched.store_tax_lines = v.value ? JSON.parse(v.value) : null;
|
|
92
|
+
}
|
|
93
|
+
if ('product_id' in b && 'tax_lines' in b) { // S3:单品覆盖(null=回落店铺)
|
|
94
|
+
const v = validateTaxLines(b.tax_lines);
|
|
95
|
+
if ('error' in v)
|
|
96
|
+
return void errorRes(res, 400, 'BAD_TAX_LINES', v.error);
|
|
97
|
+
const prodT = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
98
|
+
if (!prodT)
|
|
99
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
100
|
+
if (prodT.seller_id !== user.id)
|
|
101
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
|
|
102
|
+
await dbRun('UPDATE products SET tax_lines = ? WHERE id = ?', [v.value, b.product_id]);
|
|
103
|
+
touched.product_tax_lines = v.value ? JSON.parse(v.value) : null;
|
|
104
|
+
}
|
|
105
|
+
if ('product_id' in b && 'quote_ok' in b) {
|
|
106
|
+
if (b.quote_ok !== null && typeof b.quote_ok !== 'boolean')
|
|
107
|
+
return void errorRes(res, 400, 'BAD_QUOTE_OK', 'quote_ok 只允许 true|false|null');
|
|
108
|
+
const prodQ = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
109
|
+
if (!prodQ)
|
|
110
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
111
|
+
if (prodQ.seller_id !== user.id)
|
|
112
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品');
|
|
113
|
+
await dbRun('UPDATE products SET shipping_quote_ok = ? WHERE id = ?', [b.quote_ok === null ? null : (b.quote_ok ? 1 : 0), b.product_id]);
|
|
114
|
+
touched.product_quote_ok = b.quote_ok;
|
|
115
|
+
}
|
|
116
|
+
if ('template' in b) { // 单品运费模板:仅在显式带 template 时进入 —— 关闭整类隐患(此前 'product_id' in b 会让 {product_id, store_*} 等组合尾随进来把模板静默清成 NULL;审计 P2)。清除走 {product_id, template:null}('template' in b 成立)
|
|
117
|
+
if (!b.product_id)
|
|
118
|
+
return void errorRes(res, 400, 'MISSING_PRODUCT_ID', '设置单品运费模板须带 product_id');
|
|
119
|
+
const p = parseShippingTemplate(b.template);
|
|
120
|
+
if (!p.ok)
|
|
121
|
+
return void errorRes(res, 400, 'BAD_SHIPPING_TEMPLATE', p.error);
|
|
122
|
+
const prod = await dbOne('SELECT seller_id FROM products WHERE id = ?', [b.product_id]);
|
|
123
|
+
if (!prod)
|
|
124
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
125
|
+
if (prod.seller_id !== user.id)
|
|
126
|
+
return void errorRes(res, 403, 'NOT_PRODUCT_OWNER', '只能设置自己商品的运费模板');
|
|
127
|
+
await dbRun('UPDATE products SET shipping_template = ? WHERE id = ?', [p.entries ? JSON.stringify(p.entries) : null, b.product_id]);
|
|
128
|
+
touched.product_template = p.entries;
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(touched).length === 0)
|
|
131
|
+
return void errorRes(res, 400, 'NOTHING_TO_SET', '未提供任何设置项');
|
|
132
|
+
return void res.json({ success: true, ...touched });
|
|
133
|
+
});
|
|
134
|
+
// 卖家读自己的店铺级设置(设置 UI 回显):接单模式 + 运费模板 + 询价开关。
|
|
135
|
+
app.get('/api/seller/shipping-settings', async (req, res) => {
|
|
136
|
+
const user = auth(req, res);
|
|
137
|
+
if (!user)
|
|
138
|
+
return;
|
|
139
|
+
if (user.role !== 'seller')
|
|
140
|
+
return void errorRes(res, 403, 'SELLER_ONLY', '仅卖家');
|
|
141
|
+
const row = await dbOne('SELECT store_accept_mode, store_shipping_template, store_shipping_quote_ok, store_sale_regions, store_free_shipping_threshold, store_import_duty_terms, store_tax_lines FROM users WHERE id = ?', [user.id]);
|
|
142
|
+
return void res.json({
|
|
143
|
+
store_accept_mode: row?.store_accept_mode ?? null,
|
|
144
|
+
store_sale_regions: parseSaleRegionsRule(row?.store_sale_regions ?? null),
|
|
145
|
+
store_free_shipping_threshold: row?.store_free_shipping_threshold ?? null,
|
|
146
|
+
store_import_duty_terms: (row?.store_import_duty_terms === 'ddu' || row?.store_import_duty_terms === 'ddp') ? row.store_import_duty_terms : null,
|
|
147
|
+
store_tax_lines: parseTaxLines(row?.store_tax_lines ?? null),
|
|
148
|
+
store_template: loadTemplateJson(row?.store_shipping_template),
|
|
149
|
+
store_quote_ok: row?.store_shipping_quote_ok == null ? null : Number(row.store_shipping_quote_ok) === 1,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
// 公开读:买家下单前查配送范围。生效 = 单品覆盖 ?? 店铺默认;template=null → 不按地区计费(下单不要求选地区)。
|
|
153
|
+
app.get('/api/products/:id/shipping-options', async (req, res) => {
|
|
154
|
+
const prod = await dbOne('SELECT id, seller_id, shipping_template, shipping_quote_ok, import_duty_terms, tax_lines, sale_regions, free_shipping_threshold FROM products WHERE id = ?', [req.params.id]);
|
|
155
|
+
if (!prod)
|
|
156
|
+
return void errorRes(res, 404, 'PRODUCT_NOT_FOUND', '商品不存在');
|
|
157
|
+
let entries = loadTemplateJson(prod.shipping_template);
|
|
158
|
+
let source = entries ? 'product' : null;
|
|
159
|
+
const seller = await dbOne('SELECT store_shipping_template, store_shipping_quote_ok, store_import_duty_terms, store_tax_lines, store_sale_regions, store_free_shipping_threshold FROM users WHERE id = ?', [prod.seller_id]);
|
|
160
|
+
if (!entries) {
|
|
161
|
+
entries = loadTemplateJson(seller?.store_shipping_template);
|
|
162
|
+
if (entries)
|
|
163
|
+
source = 'store';
|
|
164
|
+
}
|
|
165
|
+
const quoteOk = prod.shipping_quote_ok != null ? Number(prod.shipping_quote_ok) === 1 : Number(seller?.store_shipping_quote_ok) === 1;
|
|
166
|
+
// S3 税费/进口责任披露(买家下单前可见;卖家 seller-declared,平台不算不收)。
|
|
167
|
+
// 价内含税【按收货地区过滤】(?ship_to_region):目的区匹配项 + '*' 通用项;无地区参数=仅 '*'(只披露必然适用的,
|
|
168
|
+
// 不把不属于该目的地的税误示给买家)。DDP/DDU 是整单单一声明,不按区分。
|
|
169
|
+
const importDuty = effectiveImportDutyTerms(prod.import_duty_terms, seller?.store_import_duty_terms);
|
|
170
|
+
const shipToRegion = typeof req.query.ship_to_region === 'string' && req.query.ship_to_region.trim() ? req.query.ship_to_region.trim().toUpperCase() : null;
|
|
171
|
+
const taxLines = taxLinesForRegion(effectiveTaxLines(prod.tax_lines, seller?.store_tax_lines), shipToRegion);
|
|
172
|
+
// S5 买家下单前聚合(只读,复用 S1-S4 判定;不动订单金额/不收税)。
|
|
173
|
+
const saleRule = parseSaleRegionsRule(prod.sale_regions) ?? parseSaleRegionsRule(seller?.store_sale_regions ?? null); // 可售规则(商品??店铺)
|
|
174
|
+
// 平台合规名单:与建单门 gateSaleRegionForCreate 共用 parsePlatformBlocklist,坏配置 fail-closed 一致(建单 503 ⇄ 预检 platform_policy_invalid),
|
|
175
|
+
// 不再把坏配置当空名单静默放行(否则预检显示"可买"、提交才 503,与 create gate 不一致)。缺省 '[]'/无行=空名单=可售。
|
|
176
|
+
const pr = await dbOne("SELECT value FROM protocol_params WHERE key = 'trade.platform_region_blocklist'").catch(() => null);
|
|
177
|
+
const parsedBlock = parsePlatformBlocklist(pr ? pr.value : '[]');
|
|
178
|
+
const policyInvalid = !parsedBlock.ok;
|
|
179
|
+
const platformBlock = parsedBlock.ok ? parsedBlock.list : [];
|
|
180
|
+
const freeThreshold = (prod.free_shipping_threshold != null && Number(prod.free_shipping_threshold) > 0) ? Number(prod.free_shipping_threshold)
|
|
181
|
+
: (seller?.store_free_shipping_threshold != null && Number(seller.store_free_shipping_threshold) > 0 ? Number(seller.store_free_shipping_threshold) : null);
|
|
182
|
+
// 可售裁定(镜像建单 gateSaleRegionForCreate 的判定,只读不抛):平台配置异常 > 平台合规 > 商家意愿。
|
|
183
|
+
const restricted = policyInvalid || !!saleRule || platformBlock.length > 0;
|
|
184
|
+
let sellable = { ok: true, reason: 'ok' };
|
|
185
|
+
if (policyInvalid)
|
|
186
|
+
sellable = { ok: false, reason: 'platform_policy_invalid' }; // 坏配置 fail-closed:显示不可下单,不诱导买家提交后才 503
|
|
187
|
+
else if (restricted) {
|
|
188
|
+
if (!shipToRegion)
|
|
189
|
+
sellable = { ok: false, reason: 'region_required' };
|
|
190
|
+
else if (platformBlock.includes(shipToRegion))
|
|
191
|
+
sellable = { ok: false, reason: 'product_restricted' }; // 平台合规,商家不可放宽
|
|
192
|
+
else if (saleRule && !regionAllowedByRule(saleRule, shipToRegion))
|
|
193
|
+
sellable = { ok: false, reason: 'region_not_for_sale' }; // 商家不销往
|
|
194
|
+
}
|
|
195
|
+
// 目的区运费裁定(有模板时;免邮阈值仅回传数值供 UI 提示"满 X 免邮",实际是否免取决于买家券后货款,建单时定)
|
|
196
|
+
let resolvedShipping = null;
|
|
197
|
+
if (entries && shipToRegion) {
|
|
198
|
+
const r = resolveShipping(entries, shipToRegion);
|
|
199
|
+
resolvedShipping = { covered: r.covered, fee: r.covered ? r.fee : null, est_days: r.est_days, quote_required: !r.covered && quoteOk };
|
|
200
|
+
}
|
|
201
|
+
return void res.json({
|
|
202
|
+
product_id: prod.id,
|
|
203
|
+
region_required: !!entries || restricted, // S5:可售/合规限制也需买家指定目的地(与建单 SHIP_REGION_REQUIRED 一致)
|
|
204
|
+
template: entries,
|
|
205
|
+
source,
|
|
206
|
+
sellable, // S5:{ ok, reason: ok|region_required|product_restricted|region_not_for_sale|platform_policy_invalid }
|
|
207
|
+
sale_regions: saleRule, // S5:生效可售规则(商品??店铺){mode:'list',include:[...]} | {mode:'all',exclude:[...]} | null —— UI 据此在无运费模板时也能生成地区入口
|
|
208
|
+
resolved_shipping: resolvedShipping, // S5:目的区运费裁定(covered/fee/est_days/quote_required)| null(无模板或未选区)
|
|
209
|
+
free_shipping_threshold: freeThreshold, // S5:生效满额免邮阈值(UI 提示"满 X 免邮";实际免取决于建单时券后货款)
|
|
210
|
+
import_duty_terms: importDuty, // 'ddu'(买家到境自付关税/税)| 'ddp'(卖家已含)| null(未声明)
|
|
211
|
+
tax_included_lines: taxLines, // 价内已含税声明(如 [{region:'SG',label:'GST',rate_pct:9,kind:'included'}])
|
|
212
|
+
tax_disclosure: 'seller_declared_platform_no_collect', // S5:税费/进口责任均为卖家声明,平台不代收代缴(直付非托管)
|
|
213
|
+
quote_outside_template: !!entries && quoteOk, // 直付轨:模板外地区可询价(卖家报运费/时效,确认后付款)
|
|
214
|
+
note: entries
|
|
215
|
+
? (quoteOk ? '下单须选择收货地区;模板内地区运费自动计入,模板外地区(直付)可先询价再付款。' : '下单须选择收货地区;运费按模板计入总额(快照)。未覆盖地区暂不可下单。')
|
|
216
|
+
: (restricted ? '该商品设有可售地区限制,请选择收货地区确认。' : '该商品不按地区计运费。'),
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
package/dist/pwa/routes/snf.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
2
|
import { snfSend, snfPullInbox, snfListInbox, snfAck, snfPendingCount, snfVerify, snfDesignate, snfGetDesignation, snfNack, snfListDeadLetter, snfRevive, } from '../../layer2-business/L2-7-snf/snf-engine.js';
|
|
3
|
+
import { isEligibleArbitrator } from '../arbitrator-lifecycle.js'; // 仲裁能力唯一源=active 白名单(verify 是争议证据面,不认 legacy role)
|
|
3
4
|
export function registerSnfRoutes(app, deps) {
|
|
4
5
|
const { db, auth } = deps;
|
|
5
6
|
app.post('/api/snf/send', (req, res) => {
|
|
@@ -105,7 +106,9 @@ export function registerSnfRoutes(app, deps) {
|
|
|
105
106
|
if (!r)
|
|
106
107
|
return void res.status(404).json({ error: '消息不存在' });
|
|
107
108
|
const uid = user.id;
|
|
108
|
-
|
|
109
|
+
// 仲裁员认 active 白名单(争议证据验签是仲裁工作面);legacy role 旁路移除 —— 否则 role-only/已吊销账号可按 id
|
|
110
|
+
// 探测任意用户间私信的存在性+签名有效性,而真·白名单仲裁员(role=buyer)反被 403。admin 保持原样。
|
|
111
|
+
if (uid !== r.sender_id && uid !== r.recipient_id && !isEligibleArbitrator(db, uid).ok && user.role !== 'admin') {
|
|
109
112
|
return void res.status(403).json({ error: '无权验证' });
|
|
110
113
|
}
|
|
111
114
|
res.json(await snfVerify(db, req.params.id));
|
|
@@ -74,13 +74,13 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
74
74
|
if (!user)
|
|
75
75
|
return;
|
|
76
76
|
const purpose = String(req.body?.purpose || '').trim();
|
|
77
|
-
const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink', 'direct_pay_disclosure_ack', 'direct_pay_order_action', 'direct_receive_production_confirm', 'direct_pay_aml_review', 'direct_pay_kyb_ingress', 'direct_pay_sanctions_ingress', 'direct_pay_aml_ingress', 'direct_pay_admin_readiness', 'direct_pay_deferral_approve', 'direct_pay_deferral_reject', 'direct_pay_product_verify', 'direct_pay_store_verify', 'direct_pay_fee_prepay_record', 'direct_pay_fee_adjust', 'direct_pay_fee_refund', 'direct_receive_account_manage']);
|
|
77
|
+
const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink', 'direct_pay_disclosure_ack', 'direct_pay_order_action', 'direct_receive_production_confirm', 'direct_receive_bond_refund', 'direct_pay_bond_slash', 'direct_pay_aml_review', 'direct_pay_kyb_ingress', 'direct_pay_sanctions_ingress', 'direct_pay_aml_ingress', 'direct_pay_admin_readiness', 'direct_pay_deferral_approve', 'direct_pay_deferral_reject', 'direct_pay_product_verify', 'direct_pay_store_verify', 'direct_pay_fee_prepay_record', 'direct_pay_fee_adjust', 'direct_pay_fee_refund', 'direct_receive_account_manage', 'platform_receive_account_manage', 'direct_pay_fee_prepay_reject', 'direct_pay_fee_prepay_request_approve', 'direct_pay_payment_info_reveal', 'arbitrator_grant', 'arbitrator_suspend', 'arbitrator_reinstate', 'arbitrator_revoke', 'arbitrate', 'agent_pair_approve', 'agent_permission_approve']);
|
|
78
78
|
if (!allowed.has(purpose))
|
|
79
79
|
return void res.status(400).json({ error: 'invalid purpose' });
|
|
80
80
|
const purpose_data = req.body?.purpose_data ?? null;
|
|
81
81
|
const creds = await dbAll('SELECT id, transports FROM webauthn_credentials WHERE user_id = ?', [user.id]);
|
|
82
82
|
if (creds.length === 0)
|
|
83
|
-
return void res.status(403).json({ error: '尚未注册任何 Passkey' });
|
|
83
|
+
return void res.status(403).json({ error: '尚未注册任何 Passkey', error_code: 'NO_PASSKEY_REGISTERED' }); // 前端据此才提示注册;已注册者的取消/设备失败不误导去注册
|
|
84
84
|
const opts = await generateAuthenticationOptions({
|
|
85
85
|
rpID: rpId,
|
|
86
86
|
// H-1: 闸门必须真正 UV,不接受 "硬件按一下" 兜底
|
|
@@ -196,5 +196,5 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
196
196
|
// RFC-020 PR-B — agent delegation grants (issue/read/revoke). Co-registered with the
|
|
197
197
|
// Passkey security routes so the money-dense server.ts stays untouched. Safe scopes
|
|
198
198
|
// only; risk scopes default-hard-reject. Reuses db/auth/generateId from WebauthnDeps.
|
|
199
|
-
registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk });
|
|
199
|
+
registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk, requireHumanPresence });
|
|
200
200
|
}
|
package/dist/pwa/server.js
CHANGED
|
@@ -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';
|
|
254
|
-
import {
|
|
255
|
-
import {
|
|
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)
|
|
@@ -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
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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 });
|
|
4479
|
-
|
|
4480
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5874
|
+
// 仲裁员身份判定 — PR-B:唯一授权源 = active arbitrator_whitelist(role='arbitrator' 旁路已移除)。域逻辑见 arbitrator-lifecycle.ts。
|
|
5851
5875
|
function isEligibleArbitrator(userId) {
|
|
5852
|
-
|
|
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) {
|
|
@@ -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
|
+
}
|