@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
package/README.md
CHANGED
|
@@ -50,8 +50,9 @@ WebAZ ships an **MCP server**. MCP is an open standard, so it works with **any M
|
|
|
50
50
|
{ "mcpServers": { "webaz": { "command": "npx", "args": ["-y", "@seasonkoh/webaz"] } } }
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
-
|
|
54
|
-
- 🟢 **Network:** register at [webaz.xyz](https://webaz.xyz) (invite + Passkey = an accountable human), copy your `api_key`, and set it as `env.WEBAZ_API_KEY` → the agent
|
|
53
|
+
- 🟢 **Network read-only (default, zero-config):** with no `WEBAZ_API_KEY`, public reads (`webaz_search` / leaderboard / price history / browse) hit the **live shared network** at [webaz.xyz](https://webaz.xyz) — nothing local. Transactional tools (register / order / list / fulfill) need a key.
|
|
54
|
+
- 🟢 **Network (full):** register at [webaz.xyz](https://webaz.xyz) (invite + Passkey = an accountable human), copy your `api_key`, and set it as `env.WEBAZ_API_KEY` → the agent can transact on the **live shared network**. (`webaz_register` never self-creates an account — accounts require a real human + Passkey, by protocol.)
|
|
55
|
+
- 🟡 **Sandbox (explicit, dev/demo):** set `WEBAZ_MODE=sandbox` → a **private local SQLite playground, isolated from the live network** — safe to try the whole order flow offline. (Opt-in; the default with no key is Network read-only, not sandbox.)
|
|
55
56
|
|
|
56
57
|
Every tool result is stamped with `_mode` so the agent always knows which network it's on. There are **38 tools**; ask `webaz_info` for the live list and params. A first prompt to try:
|
|
57
58
|
|
package/README.zh-CN.md
CHANGED
|
@@ -94,16 +94,17 @@
|
|
|
94
94
|
|
|
95
95
|
### 方式一:MCP 接入(任意 agent —— agent 原生体验)
|
|
96
96
|
|
|
97
|
-
MCP server
|
|
97
|
+
MCP server 有三种模式 / The MCP runs in one of three modes:
|
|
98
98
|
|
|
99
99
|
| 模式 / Mode | 数据源 | 用途 | 如何触发 |
|
|
100
100
|
|---|---|---|---|
|
|
101
|
-
| 🟢 **NETWORK
|
|
102
|
-
|
|
|
101
|
+
| 🟢 **NETWORK 只读(默认)** | `webaz.xyz` 共享生产网络(公共读) | 无需 key 即可搜索 / 榜单 / 价格史 / 浏览**真实网络** | 未配 `WEBAZ_API_KEY`(默认,零配置) |
|
|
102
|
+
| 🟢 **NETWORK(完整)** | `webaz.xyz` 共享生产网络(带你的 `api_key`) | 真实加入网络、和别人交易 | 配了 `WEBAZ_API_KEY` |
|
|
103
|
+
| 🟡 **SANDBOX** | 本机本地 SQLite(`~/.webaz/webaz.db`) | 离线试玩 / 开发,**与全网隔离** | **显式** `WEBAZ_MODE=sandbox` |
|
|
103
104
|
|
|
104
|
-
> 🟢
|
|
105
|
+
> 🟢 无 key = 公共读打**真网络**(不是本机);要交易(注册/下单/上架/履约)需 `WEBAZ_API_KEY`。🟡 SANDBOX 需**显式开启**(`WEBAZ_MODE=sandbox`),本机隔离,**不是**真网络。每个工具结果都盖 `_mode`,一眼知道当前在哪。
|
|
105
106
|
|
|
106
|
-
**A.
|
|
107
|
+
**A. 零配置开始(NETWORK 只读)**
|
|
107
108
|
|
|
108
109
|
把下面的 server 配置加进**你的 MCP 客户端**——以 Claude Desktop 为例,编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`(Claude Code 用 `claude mcp add`;Codex / Cursor / 其它 MCP 客户端用各自的 MCP 配置文件,字段相同):
|
|
109
110
|
|
|
@@ -118,7 +119,7 @@ MCP server 有两种模式 / The MCP runs in one of two modes:
|
|
|
118
119
|
}
|
|
119
120
|
```
|
|
120
121
|
|
|
121
|
-
重启你的 MCP 客户端。`npx`
|
|
122
|
+
重启你的 MCP 客户端。`npx` 自动下载运行。**无 key 时是 NETWORK 只读**:`webaz_search` / 榜单 / 价格史 / 浏览直接读 `webaz.xyz` 真实网络(注册 / 下单 / 上架等交易工具需要 key,见 B)。想完全离线试玩全流程?设 `WEBAZ_MODE=sandbox` 进本机沙盒(仅本机有效,与全网隔离)。
|
|
122
123
|
|
|
123
124
|
**B. 真正加入网络(NETWORK)**
|
|
124
125
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { OPEN_FEE_ACCRUING_STATUSES, readAvailableFeePrepayUnits } from './direct-pay-fee-ar.js';
|
|
2
|
+
import { sellerHasPendingSlash } from './bond-slash.js'; // B3:待复核罚没提案挡退出
|
|
3
|
+
export function enumerateBondRefundBlockers(db, sellerId) {
|
|
4
|
+
const out = [];
|
|
5
|
+
// ① 在途直付单(OPEN_FEE_ACCRUING_STATUSES 已含 pending_accept..disputed 全部非终态)
|
|
6
|
+
try {
|
|
7
|
+
const ph = OPEN_FEE_ACCRUING_STATUSES.map(() => '?').join(',');
|
|
8
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${ph})`)
|
|
9
|
+
.get(sellerId, ...OPEN_FEE_ACCRUING_STATUSES).n;
|
|
10
|
+
if (n > 0)
|
|
11
|
+
out.push({ code: 'OPEN_DIRECT_PAY_ORDERS', count: n });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
out.push({ code: 'UNVERIFIABLE_OPEN_ORDERS' });
|
|
15
|
+
}
|
|
16
|
+
// ② 取消退款握手进行中(防御性)
|
|
17
|
+
try {
|
|
18
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM direct_pay_cancel_requests WHERE seller_id = ? AND status IN ('requested','refund_marked')`)
|
|
19
|
+
.get(sellerId).n;
|
|
20
|
+
if (n > 0)
|
|
21
|
+
out.push({ code: 'OPEN_CANCEL_REFUND_HANDSHAKE', count: n });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
out.push({ code: 'UNVERIFIABLE_CANCEL_REFUND' });
|
|
25
|
+
}
|
|
26
|
+
// ③ 退货流进行中(completed 直付单的退货/场外退款握手 —— ① 覆盖不到)
|
|
27
|
+
try {
|
|
28
|
+
const n = db.prepare(`SELECT COUNT(*) n FROM return_requests r JOIN orders o ON o.id = r.order_id
|
|
29
|
+
WHERE o.seller_id = ? AND o.payment_rail = 'direct_p2p'
|
|
30
|
+
AND r.status IN ('pending','accepted_pickup_pending','picked_up','await_refund','refund_marked')`)
|
|
31
|
+
.get(sellerId).n;
|
|
32
|
+
if (n > 0)
|
|
33
|
+
out.push({ code: 'OPEN_RETURN_FLOW', count: n });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
out.push({ code: 'UNVERIFIABLE_RETURNS' });
|
|
37
|
+
}
|
|
38
|
+
// ⑤ 待复核罚没提案(B3;sellerHasPendingSlash 内部 fail-closed)
|
|
39
|
+
if (sellerHasPendingSlash(db, sellerId))
|
|
40
|
+
out.push({ code: 'PENDING_SLASH_REVIEW' });
|
|
41
|
+
// ④ 平台服务费欠费(available = Σ预充值 + Σ调整 − Σ已计提;负 = 欠)
|
|
42
|
+
try {
|
|
43
|
+
if (readAvailableFeePrepayUnits(db, sellerId) < 0)
|
|
44
|
+
out.push({ code: 'UNPAID_PLATFORM_FEES' });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
out.push({ code: 'UNVERIFIABLE_FEES' });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { slashBond } from './direct-receive-deposits.js';
|
|
2
|
+
const SELLER_FAULT_RULINGS = ['refund_buyer', 'partial_refund'];
|
|
3
|
+
/** 卖家是否有待复核罚没提案(B2 退出 blockers 用)。 */
|
|
4
|
+
export function sellerHasPendingSlash(db, sellerId) {
|
|
5
|
+
try {
|
|
6
|
+
return !!db.prepare("SELECT 1 FROM bond_slash_proposals WHERE seller_id = ? AND status = 'proposed' LIMIT 1").get(sellerId);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return true;
|
|
10
|
+
} // 表读不了 → fail-closed(视为有,挡退出)
|
|
11
|
+
}
|
|
12
|
+
/** admin 发起罚没提案。校验依据争议 + deposit 状态;冷静期算成绝对截止存库。 */
|
|
13
|
+
export function proposeBondSlash(db, args) {
|
|
14
|
+
const dep = db.prepare('SELECT id, user_id, status, production_receipt_confirmed_at FROM direct_receive_deposits WHERE id = ?')
|
|
15
|
+
.get(args.depositId);
|
|
16
|
+
if (!dep)
|
|
17
|
+
return { ok: false, reason: 'deposit not found' };
|
|
18
|
+
if (dep.status !== 'locked' && dep.status !== 'refunding')
|
|
19
|
+
return { ok: false, reason: `can only propose slash on a locked/refunding bond (got '${dep.status}')` };
|
|
20
|
+
const dsp = db.prepare(`SELECT d.id, d.status, d.ruling_type, o.payment_rail, o.seller_id
|
|
21
|
+
FROM disputes d JOIN orders o ON o.id = d.order_id WHERE d.id = ?`)
|
|
22
|
+
.get(args.disputeId);
|
|
23
|
+
if (!dsp)
|
|
24
|
+
return { ok: false, reason: 'dispute not found' };
|
|
25
|
+
if (dsp.payment_rail !== 'direct_p2p')
|
|
26
|
+
return { ok: false, reason: 'dispute is not on a direct_p2p order(罚没口径仅直付争议)' };
|
|
27
|
+
if (dsp.seller_id !== dep.user_id)
|
|
28
|
+
return { ok: false, reason: 'dispute seller does not match bond owner' };
|
|
29
|
+
if (dsp.status !== 'resolved' || !dsp.ruling_type || !SELLER_FAULT_RULINGS.includes(dsp.ruling_type)) {
|
|
30
|
+
return { ok: false, reason: `dispute must be resolved with a seller-fault ruling (${SELLER_FAULT_RULINGS.join('|')}); got status='${dsp.status}' ruling='${dsp.ruling_type ?? 'none'}'` };
|
|
31
|
+
}
|
|
32
|
+
const open = db.prepare("SELECT 1 FROM bond_slash_proposals WHERE deposit_id = ? AND status = 'proposed' LIMIT 1").get(args.depositId);
|
|
33
|
+
if (open)
|
|
34
|
+
return { ok: false, reason: 'an open slash proposal already exists for this deposit' };
|
|
35
|
+
const days = Math.max(0, args.coolingDays);
|
|
36
|
+
db.prepare(`INSERT INTO bond_slash_proposals (id, deposit_id, seller_id, dispute_id, reason, status, cooling_until, proposed_by)
|
|
37
|
+
VALUES (?,?,?,?,?, 'proposed', datetime('now', ?), ?)`)
|
|
38
|
+
.run(args.proposalId, args.depositId, dep.user_id, args.disputeId, args.reason ? String(args.reason).slice(0, 300) : null, `+${days} days`, args.proposedBy);
|
|
39
|
+
return { ok: true, proposalId: args.proposalId, status: 'proposed' };
|
|
40
|
+
}
|
|
41
|
+
/** admin 撤销提案(执行前任意时刻)。 */
|
|
42
|
+
export function cancelBondSlashProposal(db, args) {
|
|
43
|
+
const row = db.prepare('SELECT status FROM bond_slash_proposals WHERE id = ?').get(args.proposalId);
|
|
44
|
+
if (!row)
|
|
45
|
+
return { ok: false, reason: 'proposal not found' };
|
|
46
|
+
if (row.status === 'cancelled')
|
|
47
|
+
return { ok: true, status: 'cancelled', already: true };
|
|
48
|
+
if (row.status !== 'proposed')
|
|
49
|
+
return { ok: false, reason: `cannot cancel from status '${row.status}'` };
|
|
50
|
+
db.prepare("UPDATE bond_slash_proposals SET status = 'cancelled', cancelled_at = datetime('now'), cancel_note = ? WHERE id = ? AND status = 'proposed'")
|
|
51
|
+
.run(args.note ? String(args.note).slice(0, 300) : null, args.proposalId);
|
|
52
|
+
return { ok: true, status: 'cancelled' };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 执行罚没:提案 proposed + 冷静期已过 → 同一事务内 CAS(proposed→executed)+ slashBond(全额 provenance
|
|
56
|
+
* + status slashed + 资格吊销)。txnId 由调用方生成(审计关联)。
|
|
57
|
+
*/
|
|
58
|
+
export function executeBondSlashProposal(db, args) {
|
|
59
|
+
const row = db.prepare('SELECT id, deposit_id, seller_id, dispute_id, reason, status, cooling_until FROM bond_slash_proposals WHERE id = ?')
|
|
60
|
+
.get(args.proposalId);
|
|
61
|
+
if (!row)
|
|
62
|
+
return { ok: false, reason: 'proposal not found' };
|
|
63
|
+
if (row.status === 'executed')
|
|
64
|
+
return { ok: true, status: 'executed', already: true };
|
|
65
|
+
if (row.status !== 'proposed')
|
|
66
|
+
return { ok: false, reason: `cannot execute from status '${row.status}'` };
|
|
67
|
+
const coolMs = Date.parse(row.cooling_until.includes('T') ? row.cooling_until : row.cooling_until.replace(' ', 'T') + 'Z');
|
|
68
|
+
const nowMs = Date.parse(args.nowIso);
|
|
69
|
+
if (!Number.isFinite(coolMs) || !Number.isFinite(nowMs))
|
|
70
|
+
return { ok: false, reason: 'unparseable cooling timestamps' };
|
|
71
|
+
if (nowMs < coolMs)
|
|
72
|
+
return { ok: false, reason: `cooling window not over (until ${row.cooling_until})` };
|
|
73
|
+
let out = { ok: true, status: 'executed' };
|
|
74
|
+
db.transaction(() => {
|
|
75
|
+
const cas = db.prepare("UPDATE bond_slash_proposals SET status = 'executed', executed_at = datetime('now'), executed_txn_id = ? WHERE id = ? AND status = 'proposed'")
|
|
76
|
+
.run(args.txnId, args.proposalId);
|
|
77
|
+
if (cas.changes !== 1)
|
|
78
|
+
throw new Error('execute race: already processed');
|
|
79
|
+
const s = slashBond(db, { depositId: row.deposit_id, txnId: args.txnId, reason: `bond_slash_proposal:${row.id} dispute:${row.dispute_id}${row.reason ? ` ${row.reason}` : ''}` });
|
|
80
|
+
if (!s.ok)
|
|
81
|
+
throw new Error(s.reason);
|
|
82
|
+
})();
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** 提案列表(admin 队列/卖家告知)。 */
|
|
86
|
+
export function listBondSlashProposals(db, filter = {}) {
|
|
87
|
+
const where = [];
|
|
88
|
+
const params = [];
|
|
89
|
+
if (filter.sellerId) {
|
|
90
|
+
where.push('p.seller_id = ?');
|
|
91
|
+
params.push(filter.sellerId);
|
|
92
|
+
}
|
|
93
|
+
if (filter.status) {
|
|
94
|
+
where.push('p.status = ?');
|
|
95
|
+
params.push(filter.status);
|
|
96
|
+
}
|
|
97
|
+
return db.prepare(`SELECT p.*, u.name AS seller_name, u.handle AS seller_handle
|
|
98
|
+
FROM bond_slash_proposals p JOIN users u ON u.id = p.seller_id
|
|
99
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY p.proposed_at DESC LIMIT 200`).all(...params);
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 商家履约保证金条款(B 决策前置,2026-07-05)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么存在:罚没/退还必须有【商家缴纳前显式同意】的合同依据 —— 没有条款文本,罚没在合同法上站不住
|
|
5
|
+
* (SG 语境:罚没条款须像"约定履约损害赔偿"而非"惩罚",penalty clause 可能不可执行,故文本刻意
|
|
6
|
+
* 采用 liquidated-damages 表述并写明用途去向)。条款版本化:缴纳申报时快照 terms_version 进 deposit 行,
|
|
7
|
+
* 新版本只影响之后的缴纳;版本串同时作为 rail-clearance 的 policy_version(可审计对齐)。
|
|
8
|
+
*
|
|
9
|
+
* 变更本文本 = 变更商家合同条款:必须 bump BOND_TERMS.version,且只对新申报生效。
|
|
10
|
+
*/
|
|
11
|
+
export const BOND_TERMS = {
|
|
12
|
+
version: 'bond-terms.v1.2026-07-05',
|
|
13
|
+
zh: `【商家履约保证金条款 v1】
|
|
14
|
+
1. 性质:本保证金是你(商家)向 WebAZ 平台缴纳的【履约担保物】(security deposit),用于担保你在直付(direct_p2p)交易中的履约义务。它不是投资、不是存款、不是买家资金,也不产生利息。
|
|
15
|
+
2. 金额与币种:按平台公示档位(当前 T0 = 500 USDC 等值;固定 token 数,档位参考 ≈ S$500,非实时汇率换算)。可按平台公示的收款账户以 USDC 或对应法币缴纳;跨币种等值由平台运营在核实到账时按公示口径认定。
|
|
16
|
+
3. 生效:缴纳后须经平台运营核实真实到账并确认锁定方才生效;申报本身不授予任何资格。
|
|
17
|
+
4. 退还:你可申请退出并退还保证金。前提是【无任何未了结的直付责任】(在途订单、售后/退货流程、争议、待复核罚没、平台服务费欠费等),并经过平台公示的冷静期;冷静期内你的直付资格暂停,期间可随时撤销申请。退还在协议外原路/约定方式完成并留痕。
|
|
18
|
+
5. 罚没(约定履约损害赔偿):仅当直付交易争议经平台仲裁【裁定你承担责任】后,平台方可发起罚没提案;提案设有公示冷静期(即你的申诉窗口),期满经人工复核后执行。你同意:被执行的罚没金额作为【约定的履约损害赔偿与生态修复金】全额转入平台处罚金专户 —— 该专户只进不出、不用于平台收益、不向个案裁决者发放,用途受治理公示约束。
|
|
19
|
+
6. 链上风险:以 USDC 缴纳时,链、地址、金额由你自行核对;错链/错地址造成的损失由你承担。
|
|
20
|
+
7. 条款版本:本条款版本化。你每次缴纳申报时同意的版本会被记录;条款更新只影响之后的新缴纳。
|
|
21
|
+
8. 你确认:已阅读并理解上述条款,自愿缴纳。`,
|
|
22
|
+
en: `[Merchant Performance Bond Terms v1]
|
|
23
|
+
1. Nature: this bond is a SECURITY DEPOSIT you (the merchant) place with the WebAZ platform to secure your fulfilment obligations in Direct Pay (direct_p2p) trades. It is not an investment, not a deposit account, not buyer funds, and bears no interest.
|
|
24
|
+
2. Amount & currency: per the published tier (currently T0 = 500 USDC equivalent; a fixed token amount — the "≈ S$500" label is a tier reference, not a live FX conversion). Payable in USDC or the corresponding fiat via the platform's published receiving accounts; cross-currency equivalence is determined by platform operations at verification time per the published basis.
|
|
25
|
+
3. Effectiveness: the bond takes effect only after platform operations verify the actual arrival of funds and confirm the lock; a declaration by itself grants nothing.
|
|
26
|
+
4. Refund: you may request exit and refund, provided you have NO outstanding Direct Pay liabilities (open orders, after-sales/return flows, disputes, pending slash reviews, unpaid platform service fees, etc.) and after the published cooling window; your Direct Pay eligibility is suspended during the window and you may cancel the request at any time. The refund is completed off-protocol via the original/agreed channel and recorded.
|
|
27
|
+
5. Slash (agreed liquidated damages): only after a Direct Pay dispute is RULED against you by platform arbitration may the platform open a slash proposal; the proposal carries a published cooling window (your appeal window) and is executed after human review. You agree that a slashed amount, as AGREED LIQUIDATED DAMAGES and ecosystem-remediation funds, is transferred in full to the platform's penalty reserve — inflow-only, never platform profit, never paid to per-case adjudicators, with usage bound by published governance.
|
|
28
|
+
6. On-chain risk: when paying in USDC you are responsible for verifying the chain, address and amount; losses from wrong-chain/wrong-address transfers are yours.
|
|
29
|
+
7. Versioning: these terms are versioned. The version you agree to at each declaration is recorded; updates affect only subsequent deposits.
|
|
30
|
+
8. You confirm you have read and understood these terms and deposit voluntarily.`,
|
|
31
|
+
};
|
package/dist/currency.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议展示币种(单一真相源)。
|
|
3
|
+
*
|
|
4
|
+
* 协议内部模拟单位对外一律显示为 **WAZ**(pre-launch 测试币;1 WAZ ≈ 1 USDC 是模拟基准,非真实汇率,无真实结算)。
|
|
5
|
+
* 历史遗留:products.currency 曾 DEFAULT 'DCP'(旧内部代号),存量行仍可能是 'DCP'。agent-facing 输出【绝不】暴露 'DCP' ——
|
|
6
|
+
* 读时经 displayCurrency() 归一化为 WAZ。底层 schema DEFAULT 的翻转 + 存量 backfill 是【独立 gated PR】(需 ALTER/回填决策),
|
|
7
|
+
* 本模块只保证【展示层】一致,不动数据。
|
|
8
|
+
*/
|
|
9
|
+
export const PROTOCOL_CURRENCY = 'WAZ';
|
|
10
|
+
/** 归一化展示币种:空 / 'DCP'(遗留内部代号)→ 'WAZ';其余原样(大写)。用于所有 agent-facing 币种字段。 */
|
|
11
|
+
export function displayCurrency(code) {
|
|
12
|
+
const c = typeof code === 'string' ? code.trim().toUpperCase() : '';
|
|
13
|
+
if (!c || c === 'DCP')
|
|
14
|
+
return PROTOCOL_CURRENCY;
|
|
15
|
+
return c;
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 非生产确认:由真人 admin/operator 在【受控/测试环境】确认(不接真实链 / PSP)。
|
|
3
|
+
* ⚠️ TEST / ADMIN-ONLY —— **绝不可用于 production go-live**。它无条件返回 confirmed=true,不代表真实 base bond 到位。
|
|
4
|
+
* 生产收款侧必须:(a) 用 `assertProductionDepositRail()` 显式拒绝本轨;且 (b) 仅在 deposit 行
|
|
5
|
+
* `production_receipt_confirmed_at` 非 NULL 时才视为担保物到位(manual 轨不写该列)。
|
|
6
|
+
*/
|
|
7
|
+
export const MANUAL_DEPOSIT_RAIL = {
|
|
8
|
+
id: 'manual',
|
|
9
|
+
isProduction: false,
|
|
10
|
+
implemented: true,
|
|
11
|
+
legalCleared: false,
|
|
12
|
+
confirmReceipt: ({ externalRef }) => ({ confirmed: true, externalRef: externalRef ?? 'manual' }),
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* operator-attested 生产轨:履约保证金走【公司企业账户(法币)/ 专项钱包地址(USDC 等)】线下/链上交存,
|
|
16
|
+
* 由 ROOT + 真人 Passkey 的运营在【核实到账后】凭 ref 记录确认。**本代码不收款、不持私钥、不签名、不划转**——
|
|
17
|
+
* 真实资金保管/退还是公司运营/托管方责任,代码只做"已核实"的记录 + 资格判定。
|
|
18
|
+
* 这是一条【已实现】的生产轨(implemented=true),但是否放行仍由 #112 rail-clearance registry(Lock B,治理默认关)决定:
|
|
19
|
+
* 在 ROOT 翻开放行开关之前,confirmProductionReceipt 仍因 Lock B 抛 → 全程 fail-closed。
|
|
20
|
+
*/
|
|
21
|
+
export const OPERATOR_ATTESTED_DEPOSIT_RAIL = {
|
|
22
|
+
id: 'operator_attested',
|
|
23
|
+
isProduction: true,
|
|
24
|
+
implemented: true,
|
|
25
|
+
legalCleared: false, // 信息位;放行由 registry/Lock B 治理开关决定,不在此置 true
|
|
26
|
+
confirmReceipt: ({ externalRef }) => ({ confirmed: true, externalRef: externalRef ?? 'operator_attested' }),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 生产闸(Lock A):生产 go-live 路径在依赖 base bond 到位前必须调用。要求 **isProduction 且 implemented**
|
|
30
|
+
* (= 这是一条建好的生产轨)。legal/治理放行是【独立的 Lock B】(assertBondRailCleared / rail-clearance registry)。
|
|
31
|
+
* 现状:manual=非生产;usdc_onchain/fiat_psp=GATED 未实现 → 均抛;operator_attested 过 Lock A,但仍被 Lock B(默认关)挡。
|
|
32
|
+
*/
|
|
33
|
+
export function assertProductionDepositRail(rail) {
|
|
34
|
+
if (!rail.isProduction || !rail.implemented) {
|
|
35
|
+
throw new Error(`deposit-rail '${rail.id}' is NOT an implemented production rail (isProduction=${rail.isProduction}, implemented=${rail.implemented}) — cannot be used for production base-bond receipt; legal/governance clearance is enforced separately by the rail-clearance registry (Lock B)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 生产收款轨【GATED】—— 真钱/链上/PSP 自动集成未接,调用即抛。implemented=false → 也过不了 Lock A。 */
|
|
39
|
+
function gated(id) {
|
|
40
|
+
return {
|
|
41
|
+
id, isProduction: true, implemented: false, legalCleared: false,
|
|
42
|
+
confirmReceipt: () => { throw new Error(`deposit-rail '${id}' is GATED: real-money/on-chain/PSP receipt not built (legal review: security-deposit characterisation + USDC DTSP + real-money boundary required)`); },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** 取存款轨实现。manual=非生产;operator_attested=运营核实(已实现,放行仍由 Lock B 治理开关);usdc/fiat=GATED。显式 switch。 */
|
|
46
|
+
export function getDepositRail(railId) {
|
|
47
|
+
if (railId === 'manual')
|
|
48
|
+
return MANUAL_DEPOSIT_RAIL;
|
|
49
|
+
if (railId === 'operator_attested')
|
|
50
|
+
return OPERATOR_ATTESTED_DEPOSIT_RAIL;
|
|
51
|
+
return gated(railId);
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 治理可调 param 描述(供未来 launch-policy PR seed 进 DEFAULT_PARAMS;本 PR【不】改 server.ts —— 它已到 LOC 上限,
|
|
3
|
+
* 且默认值全 inert,monitor 经 getProtocolParam fallback 即用)。默认值是本模块各阈值的【单一真相源】(num() 回落到此)。
|
|
4
|
+
*/
|
|
5
|
+
export const DIRECT_PAY_AML_PARAMS = [
|
|
6
|
+
{ key: 'direct_pay.aml.window_hours', value: '24', type: 'number', description: 'Direct Pay AML 监控回看窗口(小时)。默认 24。', category: 'system', min: 1 },
|
|
7
|
+
{ key: 'direct_pay.aml.velocity_max_orders', value: '0', type: 'number', description: 'Direct Pay AML velocity 规则:窗口内卖家 direct_p2p 单数达此值即 flag(medium/open)。0=关闭(默认 inert)。', category: 'system', min: 0 },
|
|
8
|
+
{ key: 'direct_pay.aml.small_order_amount', value: '0', type: 'number', description: 'Direct Pay AML concentration 规则:"小额单"判定阈值(与 orders.total_amount 同标度)。0=关闭。', category: 'system', min: 0 },
|
|
9
|
+
{ key: 'direct_pay.aml.concentration_max_small_orders', value: '0', type: 'number', description: 'Direct Pay AML concentration 规则:窗口内小额单数达此值即 flag;需与 small_order_amount 同时 >0 方激活。0=关闭。', category: 'system', min: 0 },
|
|
10
|
+
];
|
|
11
|
+
/** 读非负数 param,脏值/缺失回落默认(默认 inert)。 */
|
|
12
|
+
function num(gp, key, fb) {
|
|
13
|
+
const v = Number(gp(key, fb));
|
|
14
|
+
return Number.isFinite(v) && v >= 0 ? v : fb;
|
|
15
|
+
}
|
|
16
|
+
/** ISO → SQLite datetime('now') 同格式 'YYYY-MM-DD HH:MM:SS'(UTC,秒级),以便与 orders.created_at 做可比的字符串比较。 */
|
|
17
|
+
function toSqliteTs(iso) {
|
|
18
|
+
return new Date(iso).toISOString().slice(0, 19).replace('T', ' ');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* direct_p2p 建单成功后运行。按治理阈值检测窗口内卖家行为,命中即 append aml_flags。返回写入的 flag id 列表(可空)。
|
|
22
|
+
* 纯写 aml_flags(append-only);无其它副作用。绝不阻断/回滚当前订单。
|
|
23
|
+
*/
|
|
24
|
+
export function runDirectPayAmlMonitor(db, args) {
|
|
25
|
+
const { sellerId, orderId, nowIso, getProtocolParam: gp } = args;
|
|
26
|
+
const windowHours = num(gp, 'direct_pay.aml.window_hours', 24) || 24;
|
|
27
|
+
const maxOrders = num(gp, 'direct_pay.aml.velocity_max_orders', 0);
|
|
28
|
+
const smallAmt = num(gp, 'direct_pay.aml.small_order_amount', 0);
|
|
29
|
+
const maxSmall = num(gp, 'direct_pay.aml.concentration_max_small_orders', 0);
|
|
30
|
+
const since = toSqliteTs(new Date(new Date(nowIso).getTime() - windowHours * 3600_000).toISOString());
|
|
31
|
+
const flagsWritten = [];
|
|
32
|
+
const write = (rule, detail) => {
|
|
33
|
+
const id = `amlf_${rule}_${orderId}`;
|
|
34
|
+
const r = db.prepare(`INSERT OR IGNORE INTO aml_flags (id, subject_user_id, related_order_id, rule, severity, detail, status, disposition)
|
|
35
|
+
VALUES (?,?,?,?, 'medium', ?, 'open', 'review_queue')`).run(id, sellerId, orderId, rule, JSON.stringify(detail));
|
|
36
|
+
if (r.changes === 1)
|
|
37
|
+
flagsWritten.push(id);
|
|
38
|
+
};
|
|
39
|
+
// Rule 1 — velocity / cumulative:窗口内卖家 direct_p2p 单数 ≥ 阈值(阈值 >0 方激活)。
|
|
40
|
+
if (maxOrders > 0) {
|
|
41
|
+
const row = db.prepare("SELECT COUNT(*) n FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND created_at >= ?").get(sellerId, since);
|
|
42
|
+
if (row.n >= maxOrders)
|
|
43
|
+
write('velocity', { window_hours: windowHours, order_count: row.n, threshold: maxOrders });
|
|
44
|
+
}
|
|
45
|
+
// Rule 2 — concentration:窗口内小额(total_amount ≤ smallAmt)direct_p2p 单数 ≥ 阈值(两阈值都 >0 方激活)。
|
|
46
|
+
if (smallAmt > 0 && maxSmall > 0) {
|
|
47
|
+
const row = db.prepare("SELECT COUNT(*) n FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND total_amount <= ? AND created_at >= ?").get(sellerId, smallAmt, since);
|
|
48
|
+
if (row.n >= maxSmall)
|
|
49
|
+
write('concentration', { window_hours: windowHours, small_order_count: row.n, small_order_amount: smallAmt, threshold: maxSmall });
|
|
50
|
+
}
|
|
51
|
+
return { flagsWritten };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* fail-soft 包装:监控写入异常【不得】破坏已提交的建单。建单成功后调用此函数(而非裸 runDirectPayAmlMonitor),
|
|
55
|
+
* 任何异常被吞并返回 { ok:false, error };绝不向上抛、绝不影响订单响应。
|
|
56
|
+
*/
|
|
57
|
+
export function safeRunDirectPayAmlMonitor(db, args) {
|
|
58
|
+
try {
|
|
59
|
+
const { flagsWritten } = runDirectPayAmlMonitor(db, args);
|
|
60
|
+
return { ok: true, flagsWritten };
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// 仅记录;绝不抛(订单已提交,监控失败不能回流成建单失败)。
|
|
64
|
+
console.error('[direct-pay-aml-monitor] non-fatal:', e.message);
|
|
65
|
+
return { ok: false, error: e.message };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
const VALID_DECISIONS = new Set(['clear', 'escalate', 'suspend']);
|
|
3
|
+
/**
|
|
4
|
+
* 复核单条 aml_flag(唯一受控 writer)。成功返回 { ok:true, ...新状态 };参数/flag 非法返回 { ok:false, error }。
|
|
5
|
+
* 绝不抛(参数与存在性都显式校验)。
|
|
6
|
+
*/
|
|
7
|
+
export function reviewAmlFlag(db, args) {
|
|
8
|
+
const { flagId, reviewerId, decision } = args; // notes: 入参契约的一部分,但刻意不持久化(见文件头 PII/schema 说明)
|
|
9
|
+
if (!flagId || !reviewerId)
|
|
10
|
+
return { ok: false, error: 'MISSING_ARGS' };
|
|
11
|
+
if (!VALID_DECISIONS.has(decision))
|
|
12
|
+
return { ok: false, error: 'INVALID_DECISION' };
|
|
13
|
+
const flag = db.prepare('SELECT id, subject_user_id, status FROM aml_flags WHERE id = ?')
|
|
14
|
+
.get(flagId);
|
|
15
|
+
if (!flag)
|
|
16
|
+
return { ok: false, error: 'FLAG_NOT_FOUND' };
|
|
17
|
+
// decision → (新 status, 新 disposition)。suspend 保留当前 status,只强制 disposition(breaker suspend 优先 → 继续阻断)。
|
|
18
|
+
let newStatus, newDisposition;
|
|
19
|
+
if (decision === 'clear') {
|
|
20
|
+
newStatus = 'cleared';
|
|
21
|
+
newDisposition = 'downgrade';
|
|
22
|
+
}
|
|
23
|
+
else if (decision === 'escalate') {
|
|
24
|
+
newStatus = 'escalated';
|
|
25
|
+
newDisposition = 'review_queue';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
newStatus = flag.status;
|
|
29
|
+
newDisposition = 'suspend';
|
|
30
|
+
}
|
|
31
|
+
db.transaction(() => {
|
|
32
|
+
// 只改复核相关列(status / disposition / reviewed_by / reviewed_at);notes 不落库(见文件头)。
|
|
33
|
+
db.prepare(`UPDATE aml_flags SET status = ?, disposition = ?, reviewed_by = ?, reviewed_at = datetime('now') WHERE id = ?`)
|
|
34
|
+
.run(newStatus, newDisposition, reviewerId, flagId);
|
|
35
|
+
// 审计(append-only;无 PII:仅内部 id + 决策 + 结果)。与 UPDATE 同事务 → 改 flag 必留痕。
|
|
36
|
+
db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
|
|
37
|
+
.run('amlrev_' + randomUUID(), reviewerId, 'direct_pay.aml_review', 'aml_flag', flagId, JSON.stringify({ subject_user_id: flag.subject_user_id, decision, status: newStatus, disposition: newDisposition }));
|
|
38
|
+
})();
|
|
39
|
+
return { ok: true, flagId, subjectUserId: flag.subject_user_id, decision: decision, status: newStatus, disposition: newDisposition };
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { sellerHasProductionBaseBondLocked } from './direct-receive-deposits.js';
|
|
2
|
+
import { getActiveDeferral } from './direct-receive-deferral.js';
|
|
3
|
+
export function sellerBaseBondEntrySatisfied(db, sellerId, nowIso) {
|
|
4
|
+
return sellerHasProductionBaseBondLocked(db, sellerId) || getActiveDeferral(db, sellerId, nowIso) != null;
|
|
5
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct Pay (Rail 1) — merchant production base-bond RAIL-CLEARANCE registry (Phase 4 scaffold)。
|
|
3
|
+
*
|
|
4
|
+
* 这是 production base-bond rail 走向上线前的【合规放行建模层】,默认【全 fail-closed】。它【不】启用真实生产收款、
|
|
5
|
+
* 【不】接 USDC/on-chain/PSP/bank、【不】实现 confirmReceipt 真实验证、【不】移动任何资金。
|
|
6
|
+
*
|
|
7
|
+
* 双锁模型(两把【独立】的锁,缺一即拒,confirmProductionReceipt 同时要求):
|
|
8
|
+
* Lock A — 已实现锁:DepositRail 必须 isProduction && implemented(是不是一条建好的生产轨)。
|
|
9
|
+
* manual=非生产;usdc/fiat=GATED(implemented=false);operator_attested=已实现 → 过 Lock A。由 assertProductionDepositRail 守。
|
|
10
|
+
* Lock B — registry 放行锁(本模块):某 rail 的 legal_cleared + production_ready + 非占位 policy_version +
|
|
11
|
+
* jurisdiction ∈ allowlist。当前【全部默认 fail-closed】→ 全拒。由 assertBondRailCleared 守。
|
|
12
|
+
*
|
|
13
|
+
* 现状(2026-07-05 更新):**operator_attested 轨已放行(SG)** —— Holden 决策 B(经营者知情自担风险,
|
|
14
|
+
* 非律师意见):保证金是商家履约担保物、不涉买家资金/货款;法币收取=常规合同担保物;USDC 收取的 PS Act
|
|
15
|
+
* DPT 定性风险已知并接受(security-deposit 豁免待律师确认,确认前敞口=pre-launch 无真实商家)。前置已建:
|
|
16
|
+
* 条款文本+缴纳前强制同意(src/bond-terms.ts,liquidated-damages 表述;policy_version=条款版本,审计对齐)。
|
|
17
|
+
* usdc_onchain / fiat_psp【自动收款轨】仍全 fail-closed(未实现+未清门,与本次放行无关)。
|
|
18
|
+
*/
|
|
19
|
+
import { getDepositRail } from './deposit-rails.js';
|
|
20
|
+
/** policy_version 占位值(= 未设)。等于此值即视为未放行。 */
|
|
21
|
+
export const BOND_POLICY_VERSION_PLACEHOLDER = 'pre-legal-unset';
|
|
22
|
+
/**
|
|
23
|
+
* 生产 base-bond rail 放行 registry。【显式 switch / 常量,无注册框架】(anti-YAGNI,镜像 deposit-rails.ts)。
|
|
24
|
+
* 只登记【生产收款轨】(usdc_onchain / fiat_psp);manual = 非生产确认轨,不在此(查询返回 null → fail-closed)。
|
|
25
|
+
* 全部默认 fail-closed —— 真值由后续【法务清门 + 外审】PR 配置,不在本 scaffold。
|
|
26
|
+
*/
|
|
27
|
+
const BOND_RAIL_CLEARANCE = {
|
|
28
|
+
// ✅ 2026-07-05 放行(Holden 决策 B,经营者知情自担;详见文件头)。policy_version = 条款版本(bond-terms.ts,
|
|
29
|
+
// 缴纳前强制同意)。运营就绪:B1 申报/核实队列 + confirm-production(ROOT+Passkey)+ B2 退还 + B3 罚没全链已建。
|
|
30
|
+
operator_attested: { railId: 'operator_attested', assetCategory: 'usdc_fiat', legalCleared: true, jurisdictionAllowlist: ['SG'], policyVersion: 'bond-terms.v1.2026-07-05', productionReady: true },
|
|
31
|
+
// 自动收款轨:未实现(Lock A 也拒),维持全 fail-closed —— 与 operator_attested 放行无关。
|
|
32
|
+
usdc_onchain: { railId: 'usdc_onchain', assetCategory: 'usdc', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
|
|
33
|
+
fiat_psp: { railId: 'fiat_psp', assetCategory: 'fiat', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
|
|
34
|
+
};
|
|
35
|
+
/** 取某 rail 的放行记录;未登记(如 manual / 未知)→ null(fail-closed)。 */
|
|
36
|
+
export function getBondRailClearance(railId) {
|
|
37
|
+
return BOND_RAIL_CLEARANCE[railId] ?? null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 该 rail 在该 jurisdiction 下是否【可用于生产 base-bond】= Lock A(真实实现)且 Lock B(registry 放行)全过。
|
|
41
|
+
* jurisdiction 语义=【平台收款主体法域】(非卖家法域)。2026-07-05 起 operator_attested/SG 为 true(#240)。纯读,无副作用。
|
|
42
|
+
*/
|
|
43
|
+
export function isBondRailClearedForProduction(railId, jurisdiction) {
|
|
44
|
+
const c = getBondRailClearance(railId);
|
|
45
|
+
if (!c)
|
|
46
|
+
return false;
|
|
47
|
+
// Lock A:已实现的生产收款轨(deposit-rails)。与 assertProductionDepositRail 一致用 implemented(非 legalCleared)——
|
|
48
|
+
// legal/治理放行全归 Lock B(本函数下半段 registry)。manual=非生产;usdc/fiat=GATED(implemented=false)。
|
|
49
|
+
let rail;
|
|
50
|
+
try {
|
|
51
|
+
rail = getDepositRail(railId);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (!rail.isProduction || !rail.implemented)
|
|
57
|
+
return false;
|
|
58
|
+
// Lock B:registry 放行(全字段)。
|
|
59
|
+
return c.legalCleared && c.productionReady
|
|
60
|
+
&& c.policyVersion !== BOND_POLICY_VERSION_PLACEHOLDER && c.policyVersion.length > 0
|
|
61
|
+
&& typeof jurisdiction === 'string' && jurisdiction.length > 0
|
|
62
|
+
&& c.jurisdictionAllowlist.includes(jurisdiction);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 生产 base-bond 放行【硬闸】(Lock B 入口):未完全放行即抛。confirmProductionReceipt 在 assertProductionDepositRail
|
|
66
|
+
* (Lock A)之后【额外】调用本闸 —— 两把独立锁都过才可能写 production receipt。当前恒抛。
|
|
67
|
+
*/
|
|
68
|
+
export function assertBondRailCleared(railId, jurisdiction) {
|
|
69
|
+
if (!isBondRailClearedForProduction(railId, jurisdiction)) {
|
|
70
|
+
throw new Error(`bond rail '${railId}' is NOT legal-cleared for production (jurisdiction='${jurisdiction}'; blockers=${bondRailClearanceBlockers(railId).join(',') || 'none'}) — production base-bond receipt cannot be confirmed`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 只读 readiness/blockers —— 列出某 rail 距离生产放行还差什么(供后续 Phase 7 readiness 面 / 运维诊断,不带 UI/endpoint)。
|
|
75
|
+
* opts.hasProductionReceipt:某具体 deposit 是否已有 production_receipt_confirmed_at(per-deposit,调用方提供事实)。
|
|
76
|
+
* 当前任何 rail 都至少含 RAIL_IMPLEMENTATION_GATED / NO_LEGAL_CLEARED_RAIL / EMPTY_JURISDICTION_ALLOWLIST / POLICY_VERSION_UNSET。
|
|
77
|
+
*/
|
|
78
|
+
export function bondRailClearanceBlockers(railId, opts) {
|
|
79
|
+
const out = [];
|
|
80
|
+
let rail = null;
|
|
81
|
+
try {
|
|
82
|
+
rail = getDepositRail(railId);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
rail = null;
|
|
86
|
+
}
|
|
87
|
+
if (!rail || !rail.isProduction || !rail.implemented)
|
|
88
|
+
out.push('RAIL_IMPLEMENTATION_GATED');
|
|
89
|
+
const c = getBondRailClearance(railId);
|
|
90
|
+
if (!c || !c.legalCleared || !c.productionReady)
|
|
91
|
+
out.push('NO_LEGAL_CLEARED_RAIL');
|
|
92
|
+
if (!c || c.jurisdictionAllowlist.length === 0)
|
|
93
|
+
out.push('EMPTY_JURISDICTION_ALLOWLIST');
|
|
94
|
+
if (!c || c.policyVersion === BOND_POLICY_VERSION_PLACEHOLDER || c.policyVersion.length === 0)
|
|
95
|
+
out.push('POLICY_VERSION_UNSET');
|
|
96
|
+
if (opts?.hasProductionReceipt !== true)
|
|
97
|
+
out.push('NO_PRODUCTION_RECEIPT');
|
|
98
|
+
return out;
|
|
99
|
+
}
|