@seasonkoh/webaz 0.1.28 → 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/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- 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 +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -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-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -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 +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
- 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/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/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- 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-ai.js +10 -10
- 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 +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -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-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -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-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- 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-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -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-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -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-listings.js +4 -4
- 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-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 +10 -10
- 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 +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- 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/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-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- 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 +248 -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/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/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- 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 +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- 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/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/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -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 +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -4,6 +4,8 @@ import { getCasesForRole, getCasesForMaintainer, validateCaseReviews } from '../
|
|
|
4
4
|
// RFC-016 Phase 1 — 申请/题目/案例/申诉的校验读 + 列表读 + 单语句写 → async seam;
|
|
5
5
|
// activate/resign/resolve-appeal 的 3 个角色态 CAS db.transaction 保持同步(Phase 3 迁 pg)。
|
|
6
6
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
7
|
+
import { grantArbitratorTx, suspendArbitrator } from '../arbitrator-lifecycle.js'; // PR-B/PR-C.2:激活桥接 active whitelist 同事务;#249-P2:仲裁员卸任真源=whitelist(suspend 可复用,非终态 revoke)
|
|
8
|
+
import { isActiveWhitelistArbitrator } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
|
|
7
9
|
// PR #22 review fix P1-2:披露文本版本(server 与 client 同步,变更披露需 bump)
|
|
8
10
|
// client 在 src/pwa/public/app.js 必须用相同 version
|
|
9
11
|
export const GOVERNANCE_APPLY_DISCLOSURE_VERSION = 'v1.0-2026-06-02';
|
|
@@ -390,10 +392,14 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
390
392
|
return void errorRes(res, 401, 'PASSKEY_INVALID', `maintainer Passkey 验证失败: ${result.reason || '未知'}`);
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
|
-
// 5. 事务:conditional UPDATE 防竞态 + INSERT activate row + 加 users.roles
|
|
395
|
+
// 5. 事务:conditional UPDATE 防竞态 + PR-C.2 仲裁授权 + INSERT activate row + 加 users.roles(仅治理记录,非 eligibility 源)
|
|
394
396
|
// PR #25 self-review fix P1-1:status check 进 transaction(防 2 maintainer 同时激活 → 2 activate row)
|
|
395
397
|
// PR #25 self-review fix P1-2:不存 note 到 appeal_resolution(语义错);maintainer note 由 logAdminAction(detail) 记
|
|
398
|
+
// PR-C.2:role='arbitrator' 的 grantArbitratorTx 与状态翻转【同事务】—— 保证"governance active ⟺ whitelist active",
|
|
399
|
+
// grant 失败(revoked/非人类/无 Passkey)→ 抛 sentinel,整个激活回滚。verifier 角色走其自身白名单,不在此桥。
|
|
396
400
|
const RACE_LOST = 'RACE_LOST_PENDING_TO_ACTIVE';
|
|
401
|
+
const GRANT_ABORT = 'GRANT_ABORT';
|
|
402
|
+
let grantErr = null;
|
|
397
403
|
try {
|
|
398
404
|
db.transaction(() => {
|
|
399
405
|
// 5.1 conditional UPDATE 原 apply row(只在 status='pending_onboarding' 时改 → 防竞态)
|
|
@@ -402,6 +408,14 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
402
408
|
// 另一 maintainer 已激活 → 抛出 sentinel,整个事务回滚
|
|
403
409
|
throw new Error(RACE_LOST);
|
|
404
410
|
}
|
|
411
|
+
// 5.1b 仲裁授权与状态翻转同事务(唯一 runtime 授权源 = active arbitrator_whitelist)
|
|
412
|
+
if (role === 'arbitrator') {
|
|
413
|
+
const g = grantArbitratorTx(db, { userId: app_.user_id, grantedBy: adminId, note: 'governance onboarding 激活' });
|
|
414
|
+
if (!g.ok) {
|
|
415
|
+
grantErr = g.error_code || 'GRANT_FAILED';
|
|
416
|
+
throw new Error(GRANT_ABORT);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
405
419
|
// 5.2 INSERT 新 row(append-only audit trail,action='activate')
|
|
406
420
|
const activateId = generateId('gapp');
|
|
407
421
|
db.prepare(`
|
|
@@ -409,7 +423,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
409
423
|
(id, user_id, role, action, status, passkey_sig, iron_rule_method)
|
|
410
424
|
VALUES (?, ?, ?, 'activate', 'active', ?, 'passkey')
|
|
411
425
|
`).run(activateId, app_.user_id, role, webauthn_token || null);
|
|
412
|
-
// 5.3 加 role 到 users.roles JSON 数组(去重)
|
|
426
|
+
// 5.3 加 role 到 users.roles JSON 数组(去重)。
|
|
427
|
+
// 仲裁员例外:仲裁资格 = active arbitrator_whitelist(5.1b 已同事务 grant,#209/#220),【不是角色】——
|
|
428
|
+
// 写进 roles 会在角色切换 UI 造出可切换的"仲裁员"假身份(与买卖身份互斥感误导用户,梦想者1号被卡案根源之一)。
|
|
429
|
+
// 普通人保留 buyer/seller 身份 + 白名单资格,仲裁台入口随 can_arbitrate 出现,买卖能力不受影响。
|
|
413
430
|
const user = db.prepare("SELECT roles FROM users WHERE id = ?").get(app_.user_id);
|
|
414
431
|
let roles = [];
|
|
415
432
|
try {
|
|
@@ -418,7 +435,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
418
435
|
catch {
|
|
419
436
|
roles = [];
|
|
420
437
|
}
|
|
421
|
-
if (!roles.includes(role)) {
|
|
438
|
+
if (role !== 'arbitrator' && !roles.includes(role)) {
|
|
422
439
|
roles.push(role);
|
|
423
440
|
db.prepare("UPDATE users SET roles = ? WHERE id = ?").run(JSON.stringify(roles), app_.user_id);
|
|
424
441
|
}
|
|
@@ -428,6 +445,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
428
445
|
if (e.message === RACE_LOST) {
|
|
429
446
|
return void errorRes(res, 409, 'ALREADY_ACTIVATED', '该 application 已被其他 maintainer 激活(竞态)');
|
|
430
447
|
}
|
|
448
|
+
if (e.message === GRANT_ABORT) { // PR-C.2:仲裁授权失败 → 整个激活已回滚
|
|
449
|
+
logAdminAction(adminId, 'governance_activate', 'user', app_.user_id, { ok: false, role, error_code: grantErr });
|
|
450
|
+
return void errorRes(res, 409, grantErr || 'GRANT_FAILED', '激活失败:无法授予仲裁员资格(见 error_code),激活已回滚');
|
|
451
|
+
}
|
|
431
452
|
throw e; // 真实 SQL error → re-throw 给 express error handler
|
|
432
453
|
}
|
|
433
454
|
// 6. admin 审计 log(maintainer note 存 detail JSON,非 governance_applications 字段)
|
|
@@ -478,7 +499,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
478
499
|
catch {
|
|
479
500
|
currentRoles = [];
|
|
480
501
|
}
|
|
481
|
-
|
|
502
|
+
// #249-P2:activate 已不再把 arbitrator 写进 users.roles(资格=白名单)→ 仲裁员的"当前在任"真源
|
|
503
|
+
// = active whitelist(或 legacy roles 残留,两者任一即可卸);verifier 真源仍 = users.roles。
|
|
504
|
+
const wlActiveArb = role === 'arbitrator' && isActiveWhitelistArbitrator(db, userId);
|
|
505
|
+
if (!currentRoles.includes(role) && !wlActiveArb) {
|
|
482
506
|
return void errorRes(res, 404, 'NOT_ACTIVE', `你当前不是 ${role},无需卸任`);
|
|
483
507
|
}
|
|
484
508
|
// 用于日志显示 + 后续 INSERT 关联(取最新的 active 行;若没有也容许,以 user.roles 为准)
|
|
@@ -534,7 +558,8 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
534
558
|
try {
|
|
535
559
|
resignId = generateId('gapp');
|
|
536
560
|
db.transaction(() => {
|
|
537
|
-
// 1.
|
|
561
|
+
// 1. 重读真源(进事务后)。仲裁员=active whitelist(#249 起 activate 不写 roles;suspend=自愿卸任可经
|
|
562
|
+
// admin reinstate 复用,刻意不用终态 revoke);legacy roles 里的 'arbitrator' 存量一并摘除。verifier=users.roles。
|
|
538
563
|
const u = db.prepare("SELECT roles FROM users WHERE id = ?").get(userId);
|
|
539
564
|
let roles = [];
|
|
540
565
|
try {
|
|
@@ -543,10 +568,23 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
|
|
|
543
568
|
catch {
|
|
544
569
|
roles = [];
|
|
545
570
|
}
|
|
546
|
-
|
|
571
|
+
const legacyInRoles = roles.includes(role);
|
|
572
|
+
if (role === 'arbitrator') {
|
|
573
|
+
const wlNow = isActiveWhitelistArbitrator(db, userId);
|
|
574
|
+
if (!wlNow && !legacyInRoles)
|
|
575
|
+
throw new Error(RACE_LOST);
|
|
576
|
+
if (wlNow) {
|
|
577
|
+
const m = suspendArbitrator(db, { userId, note: '主动卸任(governance resign)' });
|
|
578
|
+
if (!m.ok)
|
|
579
|
+
throw new Error(RACE_LOST);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (!legacyInRoles)
|
|
547
583
|
throw new Error(RACE_LOST);
|
|
548
|
-
|
|
549
|
-
|
|
584
|
+
if (legacyInRoles) {
|
|
585
|
+
roles = roles.filter(r => r !== role);
|
|
586
|
+
db.prepare("UPDATE users SET roles = ? WHERE id = ?").run(JSON.stringify(roles), userId);
|
|
587
|
+
}
|
|
550
588
|
// 2. 把所有该 user+role 的 active 行 → inactive(可能有 apply + activate 两行)
|
|
551
589
|
db.prepare("UPDATE governance_applications SET status = 'inactive' WHERE user_id = ? AND role = ? AND status = 'active'").run(userId, role);
|
|
552
590
|
// 3. INSERT resign 行(append-only audit)
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { projectDirectPayTargetForViewer } from '../direct-pay-order-redaction.js'; // 披露门:按查看者投影(物流=第三方 → 剥离收款目标)
|
|
2
3
|
export function registerLogisticsRoutes(app, deps) {
|
|
3
|
-
//
|
|
4
|
-
const { auth } = deps;
|
|
4
|
+
// 查询走 RFC-016 异步 seam(dbAll);db 仅传给共享投影器(其买家分支查 ack,本路由查看者恒第三方)
|
|
5
|
+
const { auth, db } = deps;
|
|
5
6
|
app.get('/api/logistics/companies', async (_req, res) => {
|
|
6
7
|
const companies = await dbAll(`SELECT id, name FROM users WHERE role = 'logistics' ORDER BY name ASC`);
|
|
7
8
|
res.json(companies);
|
|
@@ -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
|
+
projectDirectPayTargetForViewer(db, o, user.id);
|
|
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 { projectDirectPayTargetForViewer } 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
|
+
projectDirectPayTargetForViewer(db, o, uid); // 披露门:自导出按查看者投影(买家行=ack 门/卖家行=收款方保留),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]);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { proposeMutualCancel, acceptMutualCancel, declineMutualCancel, withdrawMutualCancel, getMutualCancelState } from '../../layer3-trust/L3-1-dispute-engine/mutual-cancel.js';
|
|
2
|
+
export function registerMutualCancelRoutes(app, deps) {
|
|
3
|
+
const { db, auth, generateId, errorRes } = deps;
|
|
4
|
+
// 域返回 error_code → HTTP 状态。未知/校验类 → 409(与当前状态冲突);系统缺失 → 500。
|
|
5
|
+
const httpFor = (code) => code === 'ORDER_NOT_FOUND' ? 404
|
|
6
|
+
: code === 'NOT_A_PARTY' ? 403
|
|
7
|
+
: code === 'SYS_MISSING' || code === 'TRANSITION_FAILED' ? 500
|
|
8
|
+
: 409;
|
|
9
|
+
app.get('/api/orders/:id/mutual-cancel', (req, res) => {
|
|
10
|
+
const user = auth(req, res);
|
|
11
|
+
if (!user)
|
|
12
|
+
return;
|
|
13
|
+
const r = getMutualCancelState(db, req.params.id, user.id);
|
|
14
|
+
if (!r.ok)
|
|
15
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'MUTUAL_CANCEL_STATE_ERROR', r.error || '读取失败');
|
|
16
|
+
res.json({ success: true, proposal: r.proposal ?? null, can_propose: !!r.can_propose, can_accept: !!r.can_accept, can_decline: !!r.can_decline, can_withdraw: !!r.can_withdraw });
|
|
17
|
+
});
|
|
18
|
+
app.post('/api/orders/:id/mutual-cancel/propose', (req, res) => {
|
|
19
|
+
const user = auth(req, res);
|
|
20
|
+
if (!user)
|
|
21
|
+
return;
|
|
22
|
+
const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
|
|
23
|
+
const r = proposeMutualCancel(db, req.params.id, user.id, reason, generateId('mcp'));
|
|
24
|
+
if (!r.ok)
|
|
25
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'MUTUAL_CANCEL_PROPOSE_ERROR', r.error || '提议失败');
|
|
26
|
+
res.json({ success: true, proposal_id: r.proposal_id, status: r.status });
|
|
27
|
+
});
|
|
28
|
+
app.post('/api/orders/:id/mutual-cancel/accept', (req, res) => {
|
|
29
|
+
const user = auth(req, res);
|
|
30
|
+
if (!user)
|
|
31
|
+
return;
|
|
32
|
+
let r;
|
|
33
|
+
try {
|
|
34
|
+
// 资金 + 状态翻转 + 争议 resolved 必须同一原子边界(RFC-016 钱路铁律);域函数内已做竞态重校验。
|
|
35
|
+
r = db.transaction(() => acceptMutualCancel(db, req.params.id, user.id))();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return void errorRes(res, 500, 'MUTUAL_CANCEL_ACCEPT_FAILED', e.message);
|
|
39
|
+
}
|
|
40
|
+
if (!r.ok)
|
|
41
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'MUTUAL_CANCEL_ACCEPT_ERROR', r.error || '确认失败');
|
|
42
|
+
res.json({ success: true, status: r.status, settlement: r.settlement });
|
|
43
|
+
});
|
|
44
|
+
app.post('/api/orders/:id/mutual-cancel/decline', (req, res) => {
|
|
45
|
+
const user = auth(req, res);
|
|
46
|
+
if (!user)
|
|
47
|
+
return;
|
|
48
|
+
const r = declineMutualCancel(db, req.params.id, user.id);
|
|
49
|
+
if (!r.ok)
|
|
50
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'MUTUAL_CANCEL_DECLINE_ERROR', r.error || '拒绝失败');
|
|
51
|
+
res.json({ success: true, status: r.status });
|
|
52
|
+
});
|
|
53
|
+
app.post('/api/orders/:id/mutual-cancel/withdraw', (req, res) => {
|
|
54
|
+
const user = auth(req, res);
|
|
55
|
+
if (!user)
|
|
56
|
+
return;
|
|
57
|
+
const r = withdrawMutualCancel(db, req.params.id, user.id);
|
|
58
|
+
if (!r.ok)
|
|
59
|
+
return void errorRes(res, httpFor(r.error_code), r.error_code || 'MUTUAL_CANCEL_WITHDRAW_ERROR', r.error || '撤回失败');
|
|
60
|
+
res.json({ success: true, status: r.status });
|
|
61
|
+
});
|
|
62
|
+
}
|