@seasonkoh/webaz 0.1.27 → 0.1.29
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/README.md +4 -3
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +13388 -22277
- package/dist/pwa/public/i18n.js +413 -196
- package/dist/pwa/public/index.html +27 -0
- package/dist/pwa/public/openapi.json +579 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +261 -1706
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1952 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +80 -2
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
// ── Anonymous-projection allowlist (security: this is a NO-AUTH public endpoint) ──────────────
|
|
3
|
+
// Each board's anon payload carries ONLY what the public value proposition needs: a public
|
|
4
|
+
// identity for linking (handle/name) + the reputation/content signal. It must NEVER carry
|
|
5
|
+
// internal canonical user ids (usr_xxx — those are the keyed inputs the #1043 cross-user-read
|
|
6
|
+
// cap rate-limits; emitting them here would be a free enumeration seed-list), account-structure
|
|
7
|
+
// metadata (keys_count), or behavior-fingerprint integers (calls_30d). ALLOWLIST, not denylist,
|
|
8
|
+
// so a future SELECT column can't silently ride to the public surface. Canonical user id (usr_xxx)
|
|
9
|
+
// is the keyed input the #1043 cross-user-read cap rate-limits, so it is DROPPED from every user
|
|
10
|
+
// board (agents/buyers/sellers/arbitrators/verifiers/creators). All user cards navigate via the
|
|
11
|
+
// public handle — #shop/:identifier and #u/:user_id both resolve a handle (users-public.ts
|
|
12
|
+
// resolveUserId: usr_xxx / permanent_code / @handle). Product boards keep `id` — a public PRODUCT
|
|
13
|
+
// id, not a user id.
|
|
14
|
+
export const BOARD_ALLOWLIST = {
|
|
15
|
+
products: ['id', 'title', 'price', 'total_likes', 'completion_count', 'seller_handle', 'seller_name', 'recommend_count', 'rank_score'],
|
|
16
|
+
value_products: ['id', 'title', 'price', 'category', 'value_badge_rank', 'value_badge_pct', 'value_badge_at', 'completion_count', 'total_likes', 'seller_handle', 'seller_name'],
|
|
17
|
+
creators: ['handle', 'name', 'region', 'products_shared', 'shareable_count', 'total_likes', 'total_clicks'], // no id — card navs #u/${handle} (#u/ resolves handle)
|
|
18
|
+
buyers: ['handle', 'name', 'region', 'orders_count'],
|
|
19
|
+
sellers: ['handle', 'name', 'region', 'orders_count', 'avg_rating', 'rating_count'], // no id — card navs via handle (#shop accepts it)
|
|
20
|
+
agents: ['handle', 'name', 'trust_score', 'level', 'activity'], // NO id / keys_count / calls_30d
|
|
21
|
+
arbitrators: ['handle', 'name', 'cases_count', 'total_yes', 'total_no', 'fairness_score'],
|
|
22
|
+
verifiers: ['handle', 'name', 'tasks_done', 'tasks_correct', 'tasks_wrong', 'accuracy', 'tier'],
|
|
23
|
+
};
|
|
24
|
+
// calls_30d (behavior fingerprint) → coarse activity bucket (keeps "busy vs dormant" product signal
|
|
25
|
+
// without leaking a raw per-account integer or the all-zero "network looks dead" aggregate).
|
|
26
|
+
function activityBucket(calls) {
|
|
27
|
+
return calls >= 20 ? 'active' : calls >= 1 ? 'quiet' : 'dormant';
|
|
28
|
+
}
|
|
29
|
+
function projectBoard(kind, rows) {
|
|
30
|
+
const allow = BOARD_ALLOWLIST[kind] || [];
|
|
31
|
+
return rows.map(r => {
|
|
32
|
+
const src = kind === 'agents' ? { ...r, activity: activityBucket(Number(r.calls_30d) || 0) } : r;
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const k of allow)
|
|
35
|
+
if (k in src)
|
|
36
|
+
out[k] = src[k];
|
|
37
|
+
return out;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
2
40
|
export function registerLeaderboardRoutes(app, deps) {
|
|
3
41
|
// db 已走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
|
|
4
42
|
const { internalAuditorId, rateLimitOk } = deps;
|
|
@@ -27,7 +65,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
27
65
|
ORDER BY rank_score DESC, p.id DESC
|
|
28
66
|
LIMIT ?
|
|
29
67
|
`, [limit]);
|
|
30
|
-
return void res.json({ kind, items: rows });
|
|
68
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
31
69
|
}
|
|
32
70
|
if (kind === 'creators') {
|
|
33
71
|
// 创作者维度:聚合自己 shareables 的总点赞 + 关联商品总数
|
|
@@ -43,7 +81,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
43
81
|
ORDER BY total_likes DESC, shareable_count DESC, u.id DESC
|
|
44
82
|
LIMIT ?
|
|
45
83
|
`, [limit]);
|
|
46
|
-
return void res.json({ kind, items: rows });
|
|
84
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
47
85
|
}
|
|
48
86
|
// B-2: 用户排行 — top buyers / sellers / verifiers
|
|
49
87
|
// 2026-05-23 隐私第一原理:移除 gmv 字段(运营状态私密,防过早 fork)
|
|
@@ -55,7 +93,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
55
93
|
WHERE o.status = 'completed' AND u.id NOT IN ('sys_protocol', ?)
|
|
56
94
|
GROUP BY u.id ORDER BY orders_count DESC, u.id DESC LIMIT ?
|
|
57
95
|
`, [internalAuditorId, limit]);
|
|
58
|
-
return void res.json({ kind, items: rows });
|
|
96
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
59
97
|
}
|
|
60
98
|
if (kind === 'sellers') {
|
|
61
99
|
// 排序改为 评分主导(avg_rating × log(1+rating_count)),不再按 GMV
|
|
@@ -70,7 +108,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
70
108
|
ORDER BY (avg_rating * (1.0 + log(1.0 + rating_count))) DESC, rating_count DESC, orders_count DESC
|
|
71
109
|
LIMIT ?
|
|
72
110
|
`, [limit]);
|
|
73
|
-
return void res.json({ kind, items: rows });
|
|
111
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
74
112
|
}
|
|
75
113
|
if (kind === 'value_products') {
|
|
76
114
|
// 2026-05-23 S5:极致性价比榜 — 按 value_badge=1 + 同类目 rank 排
|
|
@@ -85,7 +123,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
85
123
|
WHERE p.value_badge = 1 AND p.status = 'active' AND p.stock > 0
|
|
86
124
|
ORDER BY p.value_badge_rank ASC, p.value_badge_pct DESC LIMIT ?
|
|
87
125
|
`, [limit]);
|
|
88
|
-
return void res.json({ kind, items: rows });
|
|
126
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
89
127
|
}
|
|
90
128
|
if (kind === 'agents') {
|
|
91
129
|
// 2026-05-22 AG1:Agent 评测竞赛榜单
|
|
@@ -101,12 +139,12 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
101
139
|
AND acl.created_at > datetime('now', '-30 days')) as calls_30d
|
|
102
140
|
FROM agent_reputation ar
|
|
103
141
|
JOIN users u ON u.id = ar.user_id
|
|
104
|
-
WHERE u.id != 'sys_protocol'
|
|
142
|
+
WHERE u.id != 'sys_protocol' AND u.role != 'admin'
|
|
105
143
|
GROUP BY u.id
|
|
106
144
|
HAVING calls_30d > 0 OR trust_score > 0
|
|
107
145
|
ORDER BY trust_score DESC, calls_30d DESC LIMIT ?
|
|
108
146
|
`, [limit]);
|
|
109
|
-
return void res.json({ kind, items: rows });
|
|
147
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
110
148
|
}
|
|
111
149
|
if (kind === 'arbitrators') {
|
|
112
150
|
// 2026-05-22 A3:仲裁员声誉排行
|
|
@@ -132,7 +170,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
132
170
|
GROUP BY u.id
|
|
133
171
|
ORDER BY cases_count DESC, u.id DESC LIMIT ?
|
|
134
172
|
`, [limit]);
|
|
135
|
-
return void res.json({ kind, items: rows });
|
|
173
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
136
174
|
}
|
|
137
175
|
if (kind === 'verifiers') {
|
|
138
176
|
// 2026-05-22 V1:移除 tasks_done >= 5 门槛 — 小协议早期阶段会卡死榜单
|
|
@@ -152,7 +190,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
152
190
|
WHERE vs.tasks_done >= 1
|
|
153
191
|
ORDER BY vs.tasks_done DESC, u.id DESC LIMIT ?
|
|
154
192
|
`, [limit]);
|
|
155
|
-
return void res.json({ kind, items: rows });
|
|
193
|
+
return void res.json({ kind, items: projectBoard(kind, rows) });
|
|
156
194
|
}
|
|
157
195
|
return void res.json({ error: 'kind 必须是 products / creators / buyers / sellers / verifiers / arbitrators / agents / value_products' });
|
|
158
196
|
});
|
|
@@ -265,8 +265,8 @@ export function registerListingsRoutes(app, deps) {
|
|
|
265
265
|
id, seller_id, title, description, price, stock, status, images,
|
|
266
266
|
ship_regions, handling_hours, commission_rate, category_id, stake_amount,
|
|
267
267
|
listing_id, fulfillment_type, eta_hours, freshness_ts,
|
|
268
|
-
is_clearance, clearance_until, cold_start_remaining, listing_stake_locked
|
|
269
|
-
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')
|
|
268
|
+
is_clearance, clearance_until, cold_start_remaining, listing_stake_locked, currency
|
|
269
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'),?,?,?,?,'WAZ')
|
|
270
270
|
`).run(id, user.id, listing.title, (body.description ? String(body.description) : listing.description || ''), priceNum, stockNum, 'active', imagesJson, shipRegions, 24, 0.10, 'cat_default', 0, req.params.id, fulfillmentType, body.eta_hours != null ? Number(body.eta_hours) : null, body.is_clearance ? 1 : 0, body.clearance_until ? String(body.clearance_until) : null, catCfg.cold_start, stakeRequired);
|
|
271
271
|
db.prepare(`UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?`).run(stakeRequired, stakeRequired, user.id);
|
|
272
272
|
db.prepare(`UPDATE listings SET total_offers = total_offers + 1, updated_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { stripDirectPayPaymentTarget } from '../direct-pay-order-redaction.js'; // 披露门:物流第三方绝不该见收款目标
|
|
2
3
|
export function registerLogisticsRoutes(app, deps) {
|
|
3
4
|
// db 已走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
|
|
4
5
|
const { auth } = deps;
|
|
@@ -32,6 +33,9 @@ export function registerLogisticsRoutes(app, deps) {
|
|
|
32
33
|
WHERE o.logistics_id = ? AND o.status IN ('shipped','picked_up','in_transit')
|
|
33
34
|
ORDER BY o.created_at ASC LIMIT 20
|
|
34
35
|
`, [user.id]);
|
|
36
|
+
// 披露门:物流是非买家第三方,无条件删除 direct_p2p 收款目标(instruction 快照 + 账号快照);物流只需商品/状态/地址。
|
|
37
|
+
for (const o of [...available, ...mine])
|
|
38
|
+
stripDirectPayPaymentTarget(o);
|
|
35
39
|
res.json({ available, mine });
|
|
36
40
|
});
|
|
37
41
|
}
|
|
@@ -2,6 +2,9 @@ import crypto from 'crypto';
|
|
|
2
2
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
3
|
const MANIFEST_DAILY_LIMIT = 20;
|
|
4
4
|
const THUMB_MAX_BYTES = 12000; // ~12KB base64 ≈ 9KB 原始图
|
|
5
|
+
// Whitelist for stored/served thumbnails: raster data-URI only. Blocks svg/text/html data URIs so a malicious
|
|
6
|
+
// seller can't smuggle scriptable content that the public /thumb endpoint would serve (stored-XSS guard).
|
|
7
|
+
const THUMB_DATA_URI_RE = /^data:image\/(jpeg|png|webp);base64,[A-Za-z0-9+/=]+$/;
|
|
5
8
|
function verifyManifestSig(hash, ownerId, contentType, byteSize, signedAt, apiKey, signature) {
|
|
6
9
|
const payload = `${hash}|${ownerId}|${contentType}|${byteSize}|${signedAt}`;
|
|
7
10
|
const expected = crypto.createHmac('sha256', apiKey).update(payload).digest('hex');
|
|
@@ -25,6 +28,8 @@ export function registerManifestsRoutes(app, deps) {
|
|
|
25
28
|
return void res.json({ error: '请关联商品或流量口令' });
|
|
26
29
|
if (thumbnail_data_uri && thumbnail_data_uri.length > THUMB_MAX_BYTES)
|
|
27
30
|
return void res.json({ error: '缩略图过大(≤ 9KB 原始)' });
|
|
31
|
+
if (thumbnail_data_uri && !THUMB_DATA_URI_RE.test(thumbnail_data_uri))
|
|
32
|
+
return void res.json({ error: '缩略图必须是 data:image/(jpeg|png|webp);base64 格式' });
|
|
28
33
|
// 验签
|
|
29
34
|
const apiKey = me.api_key;
|
|
30
35
|
if (!verifyManifestSig(hash, me.id, content_type, byte_size, signed_at, apiKey, signature)) {
|
|
@@ -82,6 +87,39 @@ export function registerManifestsRoutes(app, deps) {
|
|
|
82
87
|
`, [req.params.hash]);
|
|
83
88
|
res.json({ manifest: m, peers });
|
|
84
89
|
});
|
|
90
|
+
// PUBLIC thumbnail bytes for <img src> (product cards can't send an auth header). Hardened:
|
|
91
|
+
// - :hash must be 64-hex (format guard)
|
|
92
|
+
// - only status='active' manifests
|
|
93
|
+
// - the stored data-URI must match the raster whitelist (jpeg/png/webp) — else 404 (no svg/text/html)
|
|
94
|
+
// - Content-Type is FORCED from the whitelisted subtype (never echoed from the stored value) + nosniff
|
|
95
|
+
// - size guard; SHORT revalidatable cache (max-age=300, must-revalidate) — NOT immutable: the content is
|
|
96
|
+
// hash-fixed but its serve-ability is not (owner/admin takedown flips status), so a long cache would
|
|
97
|
+
// keep serving a taken-down thumbnail. 5-min TTL absorbs list bursts while honoring takedown promptly.
|
|
98
|
+
// Only the low-res thumbnail is exposed (never full-res / metadata / other columns).
|
|
99
|
+
app.get('/api/manifests/:hash/thumb', async (req, res) => {
|
|
100
|
+
const hash = String(req.params.hash || '');
|
|
101
|
+
if (!/^[0-9a-f]{64}$/i.test(hash))
|
|
102
|
+
return void res.status(400).end();
|
|
103
|
+
const m = await dbOne(`SELECT thumbnail_data_uri, status FROM manifest_registry WHERE hash = ?`, [hash]);
|
|
104
|
+
if (!m || m.status !== 'active' || !m.thumbnail_data_uri)
|
|
105
|
+
return void res.status(404).end();
|
|
106
|
+
const parsed = THUMB_DATA_URI_RE.exec(m.thumbnail_data_uri);
|
|
107
|
+
if (!parsed)
|
|
108
|
+
return void res.status(404).end();
|
|
109
|
+
let buf;
|
|
110
|
+
try {
|
|
111
|
+
buf = Buffer.from(m.thumbnail_data_uri.slice(m.thumbnail_data_uri.indexOf(',') + 1), 'base64');
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return void res.status(404).end();
|
|
115
|
+
}
|
|
116
|
+
if (buf.length === 0 || buf.length > 64 * 1024)
|
|
117
|
+
return void res.status(404).end();
|
|
118
|
+
res.setHeader('Content-Type', `image/${parsed[1]}`);
|
|
119
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
120
|
+
res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
|
|
121
|
+
res.send(buf);
|
|
122
|
+
});
|
|
85
123
|
app.get('/api/manifests/by-product/:pid', async (req, res) => {
|
|
86
124
|
const user = auth(req, res);
|
|
87
125
|
if (!user)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { redactUnackedDirectPayTarget } from '../direct-pay-order-redaction.js'; // 披露门:自导出的 orders 也不得旁路
|
|
2
3
|
export function registerMeDataRoutes(app, deps) {
|
|
3
|
-
// db
|
|
4
|
-
const { auth } = deps;
|
|
4
|
+
// db 走 RFC-016 异步 seam(dbOne/dbAll);deps.db 仅供披露门(requireBothDisclosuresAcked 是同步 gate)
|
|
5
|
+
const { auth, db } = deps;
|
|
5
6
|
// COP 飞轮: 完成订单 7d 引导发笔记
|
|
6
7
|
app.get('/api/me/note-prompts', async (req, res) => {
|
|
7
8
|
const user = auth(req, res);
|
|
@@ -56,6 +57,8 @@ export function registerMeDataRoutes(app, deps) {
|
|
|
56
57
|
data.profile = await dbOne(`SELECT id, name, handle, role, region, bio, search_anchor, email, phone, permanent_code, created_at, reputation FROM users WHERE id = ?`, [uid]);
|
|
57
58
|
data.wallet = await dbOne(`SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?`, [uid]);
|
|
58
59
|
data.orders = await dbAll(`SELECT * FROM orders WHERE buyer_id = ? OR seller_id = ? ORDER BY created_at DESC LIMIT 1000`, [uid, uid]);
|
|
60
|
+
for (const o of data.orders)
|
|
61
|
+
redactUnackedDirectPayTarget(db, o, uid); // 披露门:自导出也不得在 D1/D2 ack 前泄露 direct_p2p 收款目标(JSON + CSV 同源)
|
|
59
62
|
data.shareables = await dbAll(`SELECT * FROM shareables WHERE owner_id = ? AND status != 'removed'`, [uid]);
|
|
60
63
|
data.bookmarks = await dbAll(`SELECT b.*, s.title FROM shareable_bookmarks b LEFT JOIN shareables s ON s.id = b.shareable_id WHERE b.user_id = ?`, [uid]);
|
|
61
64
|
data.likes = await dbAll(`SELECT l.*, s.title FROM shareable_likes l LEFT JOIN shareables s ON s.id = l.shareable_id WHERE l.user_id = ?`, [uid]);
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js';
|
|
2
|
+
import { releaseFeeStake } from '../../direct-pay-ledger.js'; // Rail1 直付:取消/超时释放任何遗留模拟质押(AR 订单无 stake → no-op)
|
|
3
|
+
import { requireBothDisclosuresAcked } from '../../direct-pay-disclosures.js'; // PR-4e: D1/D2 披露契约门
|
|
4
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js'; // PR-4e: 现场真人 Passkey/gate-token 门
|
|
2
5
|
export function registerOrdersActionRoutes(app, deps) {
|
|
3
|
-
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent } = deps;
|
|
6
|
+
const { db, auth, isTrustedRole, generateId, transition, notifyTransition, settleOrder, settleFault, detectFraud, createDispute, checkTimeouts, recordViolationReputation, broadcastSystemEvent, consumeGateToken } = deps;
|
|
7
|
+
// PR-4e: direct_p2p 风险动作门 —— ① D1/D2 两次披露都 ack(缺则 DISCLOSURE_NOT_ACKED);② 现场真人 Passkey + 一次性
|
|
8
|
+
// WebAuthn gate token(purpose 固定 direct_pay_order_action,order+action 走 purpose_data + validate)。
|
|
9
|
+
// 纯前置门:返回 ok 才允许后续写入。【先 disclosure(只读)再 Passkey(消费 token)】→ 缺 ack 不浪费 token。
|
|
10
|
+
// 调用方必须在【任何写入前 + 只读状态预检之后】调用,避免错误状态消耗 token。
|
|
11
|
+
function directPayActionGate(orderId, action, userId, webauthnToken) {
|
|
12
|
+
const disc = requireBothDisclosuresAcked(db, orderId);
|
|
13
|
+
if (!disc.ok)
|
|
14
|
+
return { ok: false, status: 409, error: disc.reason || '需先完成两次风险披露确认', error_code: disc.error_code || 'DISCLOSURE_NOT_ACKED' };
|
|
15
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
16
|
+
userId, webauthnToken, purpose: 'direct_pay_order_action',
|
|
17
|
+
validate: (data) => { const d = data; return !!d && d.order_id === orderId && d.action === action; },
|
|
18
|
+
});
|
|
19
|
+
if (!gate.ok)
|
|
20
|
+
return { ok: false, status: 403, error: gate.reason || '需现场真人 Passkey 确认', error_code: gate.error_code || 'HUMAN_PRESENCE_REQUIRED' };
|
|
21
|
+
return { ok: true };
|
|
22
|
+
}
|
|
4
23
|
// RFC-007 stage 2:卖家主动拒单 reason_code 白名单。
|
|
5
24
|
// classification(客观无责 vs 主观有责)是 stage 3 auto-verify 的事;stage 2 仅捕获 + 一律走违约结算。
|
|
6
25
|
// objective-claimed(stage 3 将尝试确定性核验):stock_consumed_concurrent / stale_price_snapshot / force_majeure
|
|
@@ -93,23 +112,38 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
93
112
|
return void res.status(400).json({ error: `订单状态 ${order.status} 不可确认面交` });
|
|
94
113
|
if (order.has_pending_claim)
|
|
95
114
|
return void res.status(400).json({ error: '存在进行中的验证任务,不可确认' });
|
|
115
|
+
// Rail1:平台费已切换为链下应收(accrue 在完成结算时,与 completed 同一原子边界,fail-closed)。
|
|
116
|
+
// 建单不再锁 fee-stake,故【不再】前置要求 locked stake(AR 订单本就无 stake)。
|
|
117
|
+
const isDirectP2p = order.payment_rail === 'direct_p2p';
|
|
118
|
+
// PR-4e:direct_p2p 面交完成 = RISK 动作 → 两次披露门 + 现场真人 Passkey 门。
|
|
119
|
+
if (isDirectP2p) {
|
|
120
|
+
const g = directPayActionGate(req.params.id, 'confirm_in_person', user.id, req.body?.webauthn_token);
|
|
121
|
+
if (!g.ok)
|
|
122
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
123
|
+
}
|
|
96
124
|
const tx = db.transaction(() => {
|
|
97
125
|
db.prepare(`UPDATE orders SET status='completed', updated_at=datetime('now') WHERE id = ?`).run(req.params.id);
|
|
98
126
|
db.prepare(`INSERT INTO order_state_history (id, order_id, from_status, to_status, actor_id, actor_role, notes)
|
|
99
127
|
VALUES (?,?,?,?,?,?, '面交完成 — 买家确认')`)
|
|
100
128
|
.run(generateId('hst'), req.params.id, order.status, 'completed', user.id, user.role || 'buyer');
|
|
129
|
+
// direct_p2p:取平台费进同一事务 → 取费失败(缺 locked stake)回滚 completed,订单不会 terminal-completed 而费用落空。
|
|
130
|
+
if (isDirectP2p)
|
|
131
|
+
settleOrder(req.params.id);
|
|
101
132
|
});
|
|
102
133
|
try {
|
|
103
134
|
tx();
|
|
104
135
|
}
|
|
105
136
|
catch (e) {
|
|
106
|
-
return void res.status(500).json({ error: '状态写入失败:' + e.message });
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
settleOrder(req.params.id);
|
|
137
|
+
return void res.status(isDirectP2p ? 409 : 500).json({ error: (isDirectP2p ? '直付完成结算失败,订单未完成:' : '状态写入失败:') + e.message, ...(isDirectP2p ? { error_code: 'DIRECT_PAY_SETTLE_FAILED' } : {}) });
|
|
110
138
|
}
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
// escrow:沿用原有非原子结算(失败仅记日志,不回滚 completed —— 与既有行为一致,本次不改 escrow)。
|
|
140
|
+
if (!isDirectP2p) {
|
|
141
|
+
try {
|
|
142
|
+
settleOrder(req.params.id);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.error('[settleOrder in-person]', e);
|
|
146
|
+
}
|
|
113
147
|
}
|
|
114
148
|
res.json({ success: true });
|
|
115
149
|
});
|
|
@@ -137,6 +171,32 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
137
171
|
if (action === 'confirm' && uid !== buyerId) {
|
|
138
172
|
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
139
173
|
}
|
|
174
|
+
// Rail1 直付:买家专属动作。mark_paid = 买家声明"我已付款"→ accepted(进卖家发货流程);
|
|
175
|
+
// cancel = 付款前买家取消 → cancelled(释放费用质押)。仅 buyer、仅 direct_p2p 且仍在付款窗口。
|
|
176
|
+
if (action === 'mark_paid' || action === 'cancel') {
|
|
177
|
+
if (uid !== buyerId) {
|
|
178
|
+
return void res.status(403).json({ error: '你不是本订单的买家', error_code: 'NOT_ORDER_BUYER' });
|
|
179
|
+
}
|
|
180
|
+
if (order.payment_rail !== 'direct_p2p' || order.status !== 'direct_pay_window') {
|
|
181
|
+
return void res.status(409).json({ error: '该操作仅适用于直付订单的付款窗口', error_code: 'NOT_DIRECT_PAY_WINDOW' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// PR-4e:mark_paid = direct_p2p RISK 动作(已过 direct_pay_window 只读预检)→ 两次披露门 + 现场真人 Passkey 门。cancel 不门控。
|
|
185
|
+
if (action === 'mark_paid') {
|
|
186
|
+
const g = directPayActionGate(req.params.id, 'mark_paid', uid, req.body?.webauthn_token);
|
|
187
|
+
if (!g.ok)
|
|
188
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
189
|
+
}
|
|
190
|
+
// Rail1:平台费链下应收,accrue 在完成结算时(settleOrder direct_p2p 分支)与 completed 同一原子边界、fail-closed
|
|
191
|
+
// (accrueFeeReceivable 缺费即抛 → 回滚 completed)。故【不再】前置要求 locked fee-stake。
|
|
192
|
+
// PR-4e:direct_p2p confirm = RISK 动作。先【只读状态预检】(仅 delivered 可确认收货,杜绝错误状态消耗 token),再两次披露门 + 现场真人 Passkey 门。
|
|
193
|
+
if (action === 'confirm' && order.payment_rail === 'direct_p2p') {
|
|
194
|
+
if (order.status !== 'delivered')
|
|
195
|
+
return void res.status(409).json({ error: `订单状态 ${order.status} 不可确认收货(仅 delivered)`, error_code: 'ORDER_NOT_DELIVERED' });
|
|
196
|
+
const g = directPayActionGate(req.params.id, 'confirm', uid, req.body?.webauthn_token);
|
|
197
|
+
if (!g.ok)
|
|
198
|
+
return void res.status(g.status).json({ error: g.error, error_code: g.error_code });
|
|
199
|
+
}
|
|
140
200
|
if (action === 'pickup' || action === 'transit' || action === 'deliver') {
|
|
141
201
|
// pickup 时若订单尚无物流,允许领取(孤儿单兜底)
|
|
142
202
|
const isOrphanPickup = action === 'pickup' && !logisticsId;
|
|
@@ -254,9 +314,28 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
254
314
|
note: '已就客观无责拒单发起人工仲裁举证。自动终结已暂停,等待仲裁员裁决:维持→免责全退+退质押,驳回→违约结算。',
|
|
255
315
|
});
|
|
256
316
|
}
|
|
317
|
+
// Rail1 直付:付款窗口内买家取消 → cancelled + 释放费用质押(单事务:转移成功必同步放质押,
|
|
318
|
+
// 杜绝"已取消但质押漏放"泄漏 —— cancelled 单超时 cron 不再扫,放质押无后备路径,故必须原子)。
|
|
319
|
+
if (action === 'cancel') {
|
|
320
|
+
const fromStatusCancel = order.status;
|
|
321
|
+
try {
|
|
322
|
+
db.transaction(() => {
|
|
323
|
+
const r = transition(db, req.params.id, 'cancelled', uid, [], notes);
|
|
324
|
+
if (!r.success)
|
|
325
|
+
throw new Error(r.error || '状态转移失败');
|
|
326
|
+
releaseFeeStake(db, { orderId: req.params.id });
|
|
327
|
+
})();
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
return void res.status(409).json({ error: e.message, error_code: 'CANCEL_FAILED' });
|
|
331
|
+
}
|
|
332
|
+
notifyTransition(db, req.params.id, fromStatusCancel, 'cancelled');
|
|
333
|
+
return void res.json({ success: true, status: 'cancelled', fee_stake_released: true });
|
|
334
|
+
}
|
|
257
335
|
const actionMap = {
|
|
258
336
|
accept: 'accepted', ship: 'shipped', pickup: 'picked_up',
|
|
259
|
-
transit: 'in_transit', deliver: 'delivered', confirm: 'confirmed', dispute: 'disputed'
|
|
337
|
+
transit: 'in_transit', deliver: 'delivered', confirm: 'confirmed', dispute: 'disputed',
|
|
338
|
+
mark_paid: 'accepted', // Rail1 直付:买家声明"我已付款" → accepted(汇入既有卖家发货流程;协议不验真实付款)
|
|
260
339
|
};
|
|
261
340
|
const toStatus = actionMap[action];
|
|
262
341
|
if (!toStatus)
|
|
@@ -270,6 +349,32 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
270
349
|
VALUES (?,?,?,'description',?,?,?)`).run(eid, req.params.id, user.id, evidence_description, `hash_${Date.now()}`, evReasons.length ? JSON.stringify(evReasons) : null);
|
|
271
350
|
evidenceIds.push(eid);
|
|
272
351
|
}
|
|
352
|
+
// Rail1 全原子(Codex P1):direct_p2p confirm 必须把 delivered→confirmed→completed→settle/accrue 包进【同一 db.transaction】。
|
|
353
|
+
// 否则 delivered→confirmed 先单独提交、随后 accrue 失败 → 订单卡在 confirmed(retry 被 ORDER_NOT_DELIVERED 拒)。
|
|
354
|
+
// 任一步失败 → 整体回滚到 delivered(可重试);成功后再发通知。confirm-in-person 是另一端点、已单事务原子。
|
|
355
|
+
if (action === 'confirm' && order.payment_rail === 'direct_p2p') {
|
|
356
|
+
try {
|
|
357
|
+
db.transaction(() => {
|
|
358
|
+
const r1 = transition(db, req.params.id, 'confirmed', user.id, evidenceIds, notes);
|
|
359
|
+
if (!r1.success)
|
|
360
|
+
throw new Error(r1.error || 'confirmed transition failed');
|
|
361
|
+
const r2 = transition(db, req.params.id, 'completed', 'sys_protocol', [], '系统自动结算');
|
|
362
|
+
if (!r2.success)
|
|
363
|
+
throw new Error(r2.error || 'completed transition failed');
|
|
364
|
+
settleOrder(req.params.id); // direct_p2p 分支:释放遗留模拟 stake + accrueFeeReceivable(fail-closed)
|
|
365
|
+
})();
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
return void res.status(409).json({ error: `直付完成结算失败,订单未完成(仍停在 delivered,可重试):${e.message}`, error_code: 'DIRECT_PAY_SETTLE_FAILED' });
|
|
369
|
+
}
|
|
370
|
+
notifyTransition(db, req.params.id, 'delivered', 'confirmed');
|
|
371
|
+
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
372
|
+
try {
|
|
373
|
+
broadcastSystemEvent('order_completed', '✓', `订单完成 ${req.params.id}`, req.params.id);
|
|
374
|
+
}
|
|
375
|
+
catch { }
|
|
376
|
+
return void res.json({ success: true, status: 'completed', settlement: { rail: 'direct_p2p', fee_accrued: true } });
|
|
377
|
+
}
|
|
273
378
|
const fromStatus = order.status;
|
|
274
379
|
const result = transition(db, req.params.id, toStatus, user.id, evidenceIds, notes);
|
|
275
380
|
if (!result.success)
|
|
@@ -292,6 +397,7 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
292
397
|
let settlementBreakdown = null;
|
|
293
398
|
if (toStatus === 'confirmed') {
|
|
294
399
|
const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
|
|
400
|
+
// direct_p2p confirm 已在上方【全原子早返回块】处理(delivered→confirmed→completed→settle 同一 tx);此处仅 escrow。
|
|
295
401
|
transition(db, req.params.id, 'completed', sysUser.id, [], '系统自动结算');
|
|
296
402
|
notifyTransition(db, req.params.id, 'confirmed', 'completed');
|
|
297
403
|
settleOrder(req.params.id);
|
|
@@ -339,7 +445,9 @@ export function registerOrdersActionRoutes(app, deps) {
|
|
|
339
445
|
reserveByKind.escrow_expired = round2(Number(r.s));
|
|
340
446
|
}
|
|
341
447
|
const redirectedToCommissionReserve = round2(reserveByKind.region_cap + reserveByKind.chain_gap + reserveByKind.orphan_sponsor + reserveByKind.opt_out_deactivated + reserveByKind.escrow_expired);
|
|
342
|
-
|
|
448
|
+
// RFC-018: matures_at IS NULL = opt-out escrow only; clearing rows (matures_at NOT NULL) are
|
|
449
|
+
// surfaced separately (PR3), not mislabeled as opt-out escrow here.
|
|
450
|
+
const escrowRow = (await dbOne("SELECT COALESCE(SUM(amount),0) AS s FROM pending_commission_escrow WHERE order_id = ? AND status = 'pending' AND matures_at IS NULL", [req.params.id]));
|
|
343
451
|
const heldInOptOutEscrow = round2(Number(escrowRow.s));
|
|
344
452
|
// QA 轮 9.5 P2:payouts 表只 MCP legacy 写,PWA settleOrder 直更 wallet.balance 不写 payouts
|
|
345
453
|
// 改用公式推算 sellerAmount(跟 PWA settleOrder 内部计算一致),更可靠
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandate.js';
|
|
2
2
|
// RFC-014 PR3 — 金额走整数 base-units;钱包写绝对值(防 REAL 浮点加法 dust)。
|
|
3
3
|
import { toUnits, toDecimal, mulQty, mulRate } from '../../money.js';
|
|
4
|
+
import { createDirectPayResponse } from '../../direct-pay-create.js'; // PR-4c: direct_p2p 建单分叉(生产门+收款指令门+原子建单;本金不入协议)
|
|
4
5
|
import { applyWalletDelta } from '../../ledger.js';
|
|
5
6
|
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(仅下单事务外的预检查;事务内 escrow/INSERT 保持同步)
|
|
6
7
|
// 店铺推荐 → 商品三级归因的【懒升级】(sync,跑在下单事务内、getProductShareChain 之前)。
|
|
@@ -236,6 +237,9 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
236
237
|
const insurancePremium = toDecimal(insurancePremiumU);
|
|
237
238
|
const totalAmount = toDecimal(totalAmountU);
|
|
238
239
|
const donationAmount = toDecimal(donationAmountU);
|
|
240
|
+
// PR-4c:direct_p2p 分叉 —— 本金不入协议,跳过下方 escrow 预检/事务,改走直付建单(生产门+收款指令门+原子建单,仅锁卖家 fee-stake)。
|
|
241
|
+
if (String(req.body?.payment_rail || '') === 'direct_p2p')
|
|
242
|
+
return void createDirectPayResponse(res, db, { generateId, transition, appendOrderEvent, getProtocolParam }, { product, buyerId: user.id, reqQty, basePrice, totalAmount, totalAmountU, shippingAddress: String(shipping_address), directReceiveAccountId: (typeof req.body?.direct_receive_account_id === 'string' && req.body.direct_receive_account_id) ? String(req.body.direct_receive_account_id) : undefined, opts: { variantId: variant_id, hasVariants: Number(product.has_variants) === 1, flashActive: !!flashSale, couponCode: coupon_code, buyInsurance: !!buy_insurance, donationPct: donationPctNum, isGift: !!is_gift, anonymous: anonymousFlag === 1, deliveryWindow: !!delivery_window } });
|
|
239
243
|
// 友好预检查(读):真正的守恒在下面的同步事务内(applyWalletDelta 绝对值落库)。
|
|
240
244
|
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
241
245
|
if (!wallet)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// RFC-011 §⑥ 事件游标流(纯 db 函数,party-gated)
|
|
2
2
|
import { listOrderEventsSince } from '../../layer0-foundation/L0-2-state-machine/order-chain.js';
|
|
3
3
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
4
|
+
import { requireBothDisclosuresAcked } from '../../direct-pay-disclosures.js'; // PR-4f-b: direct_p2p 收款说明响应契约门
|
|
5
|
+
import { redactUnackedDirectPayTarget } from '../direct-pay-order-redaction.js'; // 收款目标披露门(共享;所有 orders reader 必过)
|
|
6
|
+
import { getQrImageForOwner } from '../../direct-receive-account-qr.js'; // Rail1 D2:ack 门后按订单快照 qr_ref 取收款码字节((ref,seller_id) 域内)
|
|
4
7
|
export function registerOrdersReadRoutes(app, deps) {
|
|
5
8
|
const { db, auth, getOrderStatus, getOrderChain, verifyOrderChain, getOrderDispute } = deps;
|
|
6
9
|
app.get('/api/orders', async (req, res) => {
|
|
@@ -25,6 +28,7 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
25
28
|
delete o.recipient_code;
|
|
26
29
|
o.buyer_name = '🔒 ' + (typeof code === 'string' ? code : 'PR-?????');
|
|
27
30
|
}
|
|
31
|
+
redactUnackedDirectPayTarget(db, o, user.id); // #179 审计 P1:列表也过披露门,不得旁路
|
|
28
32
|
}
|
|
29
33
|
res.json(orders);
|
|
30
34
|
});
|
|
@@ -194,6 +198,38 @@ export function registerOrdersReadRoutes(app, deps) {
|
|
|
194
198
|
delete order.recipient_code;
|
|
195
199
|
}
|
|
196
200
|
}
|
|
201
|
+
// Direct Pay 响应契约门:direct_p2p 收款目标(instruction 快照 + 账号快照 qr_ref),买家在 D1/D2 both-acked 前
|
|
202
|
+
// 【不得】从 API 拿到(非仅 UI 软门)。与 /api/orders 列表共用同一 redact,防旁路(#179 审计 P1)。
|
|
203
|
+
redactUnackedDirectPayTarget(db, order, user.id);
|
|
197
204
|
res.json({ ...statusInfo, history, product, dispute, trackingInfo });
|
|
198
205
|
});
|
|
206
|
+
// Rail1 D2:直付订单收款二维码(硬化转发)。仅【订单买家】+ 两次披露 both-acked 后可取;按建单时快照的 (qr_ref, seller_id)
|
|
207
|
+
// 取【当时那一版】图字节。未 ack / 非买家 / 无 QR / 非 direct_p2p → 统一 404(不枚举,不泄露)。图字节不入 order JSON。
|
|
208
|
+
app.get('/api/orders/:id/direct-pay-qr', async (req, res) => {
|
|
209
|
+
const user = auth(req, res);
|
|
210
|
+
if (!user)
|
|
211
|
+
return;
|
|
212
|
+
const orderId = String(req.params.id);
|
|
213
|
+
const order = await dbOne('SELECT buyer_id, seller_id, payment_rail, direct_pay_account_snapshot FROM orders WHERE id = ?', [orderId]);
|
|
214
|
+
if (!order || order.payment_rail !== 'direct_p2p' || order.buyer_id !== user.id)
|
|
215
|
+
return void res.status(404).end();
|
|
216
|
+
if (!requireBothDisclosuresAcked(db, orderId).ok)
|
|
217
|
+
return void res.status(404).end(); // 未 ack:与"无 QR"同样 404,不泄露存在性
|
|
218
|
+
let snap = {};
|
|
219
|
+
try {
|
|
220
|
+
snap = order.direct_pay_account_snapshot ? JSON.parse(order.direct_pay_account_snapshot) : {};
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
snap = {};
|
|
224
|
+
}
|
|
225
|
+
if (!snap.qr_ref)
|
|
226
|
+
return void res.status(404).end();
|
|
227
|
+
const img = getQrImageForOwner(db, snap.qr_ref, order.seller_id); // (ref, seller_id) 域内取字节;seller_id 取自订单,非用户输入
|
|
228
|
+
if (!img)
|
|
229
|
+
return void res.status(404).end();
|
|
230
|
+
res.setHeader('Content-Type', img.mime);
|
|
231
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
232
|
+
res.setHeader('Cache-Control', 'private, no-store');
|
|
233
|
+
res.send(img.buf);
|
|
234
|
+
});
|
|
199
235
|
}
|
|
@@ -51,8 +51,8 @@ export function registerP2pProductsRoutes(app, deps) {
|
|
|
51
51
|
await dbRun(`
|
|
52
52
|
INSERT INTO products (id, seller_id, title, description, price, stock, status, images, ship_regions,
|
|
53
53
|
handling_hours, commission_rate, category_id, stake_amount, p2p_mode, content_hash, peer_endpoint,
|
|
54
|
-
content_signature, content_signed_at)
|
|
55
|
-
VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1
|
|
54
|
+
content_signature, content_signed_at, currency)
|
|
55
|
+
VALUES (?,?,?,?,?,?,'active',?,?,24,0.10,'cat_default',0,1,?,?,?,?,'WAZ')
|
|
56
56
|
`, [
|
|
57
57
|
id, user.id, title,
|
|
58
58
|
`[P2P] ${title}(完整详情见卖家节点)`,
|
|
@@ -23,7 +23,7 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
23
23
|
const quotaCheck = checkSellerCanList(user);
|
|
24
24
|
if (!quotaCheck.ok)
|
|
25
25
|
return void res.json({ error: quotaCheck.reason });
|
|
26
|
-
const { title, description, price, stock = 1, category = '', specs, brand, model, source_url, source_price, external_title, weight_kg, ship_regions = '全国', handling_hours = 24, estimated_days, fragile = 0, return_days = 7, return_condition = '', warranty_days = 0, commission_rate, product_type = 'retail', // 里程碑 6
|
|
26
|
+
const { title, description, price, stock = 1, category = '', specs, brand, model, source_url, source_price, external_title, weight_kg, ship_regions = '全国', handling_hours = 24, estimated_days, fragile = 0, return_days = 7, return_condition = '', warranty_days = 0, low_stock_threshold = 3, auto_delist_on_zero = 1, commission_rate, product_type = 'retail', // 里程碑 6
|
|
27
27
|
aliases = [], // 里程碑 7.2:上架时同步声明的 alias 集合
|
|
28
28
|
image_hashes = [], // 商品图片 — 只存 hash(64 hex),实际 blob 在卖家节点 IDB
|
|
29
29
|
} = req.body;
|
|
@@ -80,14 +80,16 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
80
80
|
specs, brand, model, source_url, source_price, source_price_at,
|
|
81
81
|
weight_kg, ship_regions, handling_hours, estimated_days, fragile,
|
|
82
82
|
return_days, return_condition, warranty_days,
|
|
83
|
+
low_stock_threshold, auto_delist_on_zero,
|
|
83
84
|
commitment_hash, description_hash, price_hash, hashed_at,
|
|
84
|
-
commission_rate, product_type, images
|
|
85
|
-
) VALUES (
|
|
85
|
+
commission_rate, product_type, images, currency
|
|
86
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'WAZ')`, [
|
|
86
87
|
id, user.id, title, description, priceNum, Number(stock), category, stakeAmount,
|
|
87
88
|
specsJson, brand ?? null, model ?? null,
|
|
88
89
|
source_url ?? null, source_price ? Number(source_price) : null, source_price ? now : null,
|
|
89
90
|
weight_kg ? Number(weight_kg) : null, ship_regions, Number(handling_hours), estJson, fragile ? 1 : 0,
|
|
90
91
|
Number(return_days), return_condition, Number(warranty_days),
|
|
92
|
+
Math.max(0, Math.floor(Number(low_stock_threshold) || 0)), auto_delist_on_zero ? 1 : 0,
|
|
91
93
|
makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }),
|
|
92
94
|
makePriceHash(priceNum, now), now,
|
|
93
95
|
commissionRateNum, product_type, imagesJsonForInsert
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { invalidateProductVerification } from '../../product-verification.js';
|
|
2
3
|
export function registerProductsLinksRoutes(app, deps) {
|
|
3
4
|
// 只读/单写站点走 RFC-016 异步 seam;db 保留:认领冲突分支是 fee-lock 资金路径,
|
|
4
5
|
// INSERT 链接 + INSERT 验证任务 + 钱包扣费必须原子(db.transaction + 守恒/dup guard),Phase 3 迁 pg 行锁。
|
|
@@ -15,6 +16,21 @@ export function registerProductsLinksRoutes(app, deps) {
|
|
|
15
16
|
const links = await dbAll(`SELECT id, url, source, verified, revoked, verify_note, added_at, platform, external_id, external_title FROM product_external_links WHERE product_id = ? ORDER BY added_at ASC`, [req.params.id]);
|
|
16
17
|
res.json(links);
|
|
17
18
|
});
|
|
19
|
+
// Buyer-facing read for the product detail page: VERIFIED + non-revoked links only, shape { links }.
|
|
20
|
+
// NOT owner-gated (any logged-in viewer) — separate from the owner-only /links above (which returns a bare
|
|
21
|
+
// array the seller workbench consumes; that stays untouched). Only public-safe columns are returned.
|
|
22
|
+
app.get('/api/products/:id/external-links', async (req, res) => {
|
|
23
|
+
const user = auth(req, res);
|
|
24
|
+
if (!user)
|
|
25
|
+
return;
|
|
26
|
+
// Mirror product-detail visibility (products-crud GET /:id): others see active only; the seller sees own
|
|
27
|
+
// non-active. Otherwise 404 — never leak source URLs of a warehouse/deleted product to non-owners.
|
|
28
|
+
const product = await dbOne("SELECT id FROM products WHERE id = ? AND (status = 'active' OR seller_id = ?)", [req.params.id, user.id]);
|
|
29
|
+
if (!product)
|
|
30
|
+
return void res.status(404).json({ error: 'not_found' });
|
|
31
|
+
const links = await dbAll(`SELECT url, platform, external_title FROM product_external_links WHERE product_id = ? AND verified = 1 AND revoked = 0 ORDER BY added_at ASC`, [req.params.id]);
|
|
32
|
+
res.json({ links });
|
|
33
|
+
});
|
|
18
34
|
// 新链接(无人认领)直接 verified=1;已被他人认领则发起众包验证任务
|
|
19
35
|
app.post('/api/products/:id/links', async (req, res) => {
|
|
20
36
|
const user = auth(req, res);
|
|
@@ -69,6 +85,12 @@ export function registerProductsLinksRoutes(app, deps) {
|
|
|
69
85
|
await dbRun(`INSERT INTO product_external_links
|
|
70
86
|
(id, product_id, url, source, verified, verified_at, platform, external_id, external_title)
|
|
71
87
|
VALUES (?, ?, ?, 'manual', 1, datetime('now'), ?, ?, ?)`, [linkId, req.params.id, url, meta?.platform ?? null, meta?.external_id ?? null, linkExternalTitle]);
|
|
88
|
+
try {
|
|
89
|
+
invalidateProductVerification(db, String(req.params.id));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.error('[product-verify invalidate]', e);
|
|
93
|
+
} // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
|
|
72
94
|
return void res.json({ link_id: linkId, verified: 1, external_title: linkExternalTitle, message: '链接已关联' });
|
|
73
95
|
}
|
|
74
96
|
// 已被他人认领:发起众包验证任务
|
|
@@ -129,6 +151,12 @@ export function registerProductsLinksRoutes(app, deps) {
|
|
|
129
151
|
console.error('[products-links claim tx]', msg);
|
|
130
152
|
return void res.status(500).json({ error: '发起认领失败,请重试' });
|
|
131
153
|
}
|
|
154
|
+
try {
|
|
155
|
+
invalidateProductVerification(db, String(req.params.id));
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.error('[product-verify invalidate]', e);
|
|
159
|
+
} // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
|
|
132
160
|
res.json({
|
|
133
161
|
link_id: linkId,
|
|
134
162
|
task_id: taskId,
|
|
@@ -147,6 +175,12 @@ export function registerProductsLinksRoutes(app, deps) {
|
|
|
147
175
|
if (!product || product.seller_id !== user.id)
|
|
148
176
|
return void res.status(403).json({ error: '无权限' });
|
|
149
177
|
await dbRun('DELETE FROM product_external_links WHERE id = ? AND product_id = ?', [req.params.linkId, req.params.id]);
|
|
178
|
+
try {
|
|
179
|
+
invalidateProductVerification(db, String(req.params.id));
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
console.error('[product-verify invalidate]', e);
|
|
183
|
+
} // PR-⑥ 反作弊:外链变更 → 作废直付逐品验证
|
|
150
184
|
res.json({ success: true });
|
|
151
185
|
});
|
|
152
186
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHmac } from 'crypto';
|
|
2
2
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
|
|
3
4
|
export function registerProductsListRoutes(app, deps) {
|
|
4
5
|
// db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
5
6
|
const { getUser, VALID_PRODUCT_TYPES, RAW_MODE_MIN_TRUST, getAgentTrustCached, VALID_SORTS, PRODUCT_LIMITS, TRENDING_SCORE_EXPR, findProductsByAlias, decodeProductCursor, encodeProductCursor, MASTER_SEED, formatProductForAgent } = deps;
|
|
@@ -52,7 +53,7 @@ export function registerProductsListRoutes(app, deps) {
|
|
|
52
53
|
COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level,
|
|
53
54
|
COALESCE(rs.transactions_done, 0) as seller_tx_count,
|
|
54
55
|
pc.seasonal_months as seasonal_months,
|
|
55
|
-
(SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o
|
|
56
|
+
(SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count,
|
|
56
57
|
(SELECT COUNT(DISTINCT buyer_id) FROM order_ratings r WHERE r.product_id = p.id AND r.stars >= 4) as recommend_count,
|
|
57
58
|
(SELECT COUNT(*) FROM dispute_cases dc WHERE dc.seller_id = p.seller_id) as seller_dispute_count,
|
|
58
59
|
-- 卖家仲裁胜率:无案件视为 0.8 中性(未经检验);有案件按真实比率
|