@seasonkoh/webaz 0.1.28 → 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.
Files changed (111) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/currency.js +16 -0
  4. package/dist/deposit-rails.js +52 -0
  5. package/dist/direct-pay-aml-monitor.js +67 -0
  6. package/dist/direct-pay-aml-review.js +40 -0
  7. package/dist/direct-pay-base-bond-entry.js +5 -0
  8. package/dist/direct-pay-bond-rail-clearance.js +94 -0
  9. package/dist/direct-pay-compliance-ingress.js +145 -0
  10. package/dist/direct-pay-controls.js +136 -0
  11. package/dist/direct-pay-create.js +162 -0
  12. package/dist/direct-pay-deferral-quota.js +43 -0
  13. package/dist/direct-pay-disclosures.js +44 -0
  14. package/dist/direct-pay-eligibility.js +46 -0
  15. package/dist/direct-pay-fee-ar.js +241 -0
  16. package/dist/direct-pay-launch-readiness.js +108 -0
  17. package/dist/direct-pay-launch-summary.js +68 -0
  18. package/dist/direct-pay-ledger.js +94 -0
  19. package/dist/direct-receive-account-qr.js +77 -0
  20. package/dist/direct-receive-accounts.js +82 -0
  21. package/dist/direct-receive-deferral.js +142 -0
  22. package/dist/direct-receive-deposits.js +260 -0
  23. package/dist/direct-receive-payment-instruction.js +26 -0
  24. package/dist/fx-rates.js +71 -0
  25. package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
  26. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  27. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
  28. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  29. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  30. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +18 -16
  32. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  33. package/dist/ledger.js +12 -3
  34. package/dist/mcp.js +23 -4
  35. package/dist/merchant-bond-domain.js +64 -0
  36. package/dist/merchant-bond-exposure.js +78 -0
  37. package/dist/merchant-bond-watcher.js +26 -0
  38. package/dist/payment-rails.js +29 -0
  39. package/dist/product-verification.js +93 -0
  40. package/dist/pwa/acp-feed.js +3 -2
  41. package/dist/pwa/contract-fingerprint.js +3 -0
  42. package/dist/pwa/direct-pay-guards.js +20 -0
  43. package/dist/pwa/direct-pay-order-redaction.js +24 -0
  44. package/dist/pwa/endpoint-actions.js +3 -0
  45. package/dist/pwa/public/app-ai.js +10 -10
  46. package/dist/pwa/public/app-chat-poll.js +29 -0
  47. package/dist/pwa/public/app-create-kinds.js +17 -0
  48. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  49. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  50. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  51. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  52. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  53. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  54. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  55. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  56. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  57. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  58. package/dist/pwa/public/app-direct-pay.js +227 -0
  59. package/dist/pwa/public/app-discover.js +20 -20
  60. package/dist/pwa/public/app-external-links.js +32 -0
  61. package/dist/pwa/public/app-listings.js +4 -4
  62. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  63. package/dist/pwa/public/app-price.js +55 -0
  64. package/dist/pwa/public/app-product-media.js +15 -0
  65. package/dist/pwa/public/app-profile.js +6 -6
  66. package/dist/pwa/public/app-seller.js +5 -5
  67. package/dist/pwa/public/app-shop.js +19 -19
  68. package/dist/pwa/public/app.js +142 -146
  69. package/dist/pwa/public/i18n.js +398 -197
  70. package/dist/pwa/public/index.html +17 -0
  71. package/dist/pwa/public/openapi.json +495 -1
  72. package/dist/pwa/routes/admin-analytics.js +4 -2
  73. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  74. package/dist/pwa/routes/buyer-feeds.js +2 -1
  75. package/dist/pwa/routes/chat.js +6 -1
  76. package/dist/pwa/routes/dashboards.js +2 -1
  77. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  78. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  79. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  80. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  81. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  82. package/dist/pwa/routes/fx.js +12 -0
  83. package/dist/pwa/routes/leaderboard.js +47 -9
  84. package/dist/pwa/routes/listings.js +2 -2
  85. package/dist/pwa/routes/logistics.js +4 -0
  86. package/dist/pwa/routes/manifests.js +38 -0
  87. package/dist/pwa/routes/me-data.js +5 -2
  88. package/dist/pwa/routes/orders-action.js +117 -9
  89. package/dist/pwa/routes/orders-create.js +4 -0
  90. package/dist/pwa/routes/orders-read.js +36 -0
  91. package/dist/pwa/routes/p2p-products.js +2 -2
  92. package/dist/pwa/routes/products-create.js +5 -3
  93. package/dist/pwa/routes/products-links.js +34 -0
  94. package/dist/pwa/routes/products-list.js +2 -1
  95. package/dist/pwa/routes/products-update.js +22 -2
  96. package/dist/pwa/routes/promoter.js +3 -0
  97. package/dist/pwa/routes/referral.js +4 -0
  98. package/dist/pwa/routes/returns.js +26 -1
  99. package/dist/pwa/routes/rewards-apply.js +10 -5
  100. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  101. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  102. package/dist/pwa/routes/shops.js +2 -1
  103. package/dist/pwa/routes/url-claim.js +2 -2
  104. package/dist/pwa/routes/users-public.js +8 -0
  105. package/dist/pwa/routes/wallet-read.js +3 -0
  106. package/dist/pwa/routes/webauthn.js +1 -1
  107. package/dist/pwa/server.js +59 -102
  108. package/dist/runtime/webaz-schema-helpers.js +104 -0
  109. package/dist/store-verification.js +77 -0
  110. package/dist/version.js +1 -1
  111. package/package.json +71 -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
- - 🟡 **Sandbox (default, zero-config):** with no `WEBAZ_API_KEY`, the agent runs against a **private local SQLite playground, isolated from the live network** — safe to try `webaz_register` / `webaz_search` / the whole order flow.
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 acts on the **live shared network**. (On Network, `webaz_register` never self-creates an account — accounts require a real human + Passkey, by protocol.)
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 有两种模式 / The MCP runs in one of two modes:
97
+ MCP server 有三种模式 / The MCP runs in one of three modes:
98
98
 
99
99
  | 模式 / Mode | 数据源 | 用途 | 如何触发 |
100
100
  |---|---|---|---|
101
- | 🟢 **NETWORK** | `webaz.xyz` 共享生产网络(带你的 `api_key`) | 真实加入网络、和别人交易 | 配了 `WEBAZ_API_KEY` |
102
- | 🟡 **SANDBOX** | 本机本地 SQLite(`~/.webaz/webaz.db`) | 离线试玩 / 开发,**与全网隔离** | 未配 `WEBAZ_API_KEY`(默认) |
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
- > 🟢 NETWORK = your agent acts on the live shared network. 🟡 SANDBOX = local-only, private to your machine, **not** the live network. Every tool result is stamped with `_mode` so you always know which one you're in.
105
+ > 🟢 key = 公共读打**真网络**(不是本机);要交易(注册/下单/上架/履约)需 `WEBAZ_API_KEY`。🟡 SANDBOX 需**显式开启**(`WEBAZ_MODE=sandbox`),本机隔离,**不是**真网络。每个工具结果都盖 `_mode`,一眼知道当前在哪。
105
106
 
106
- **A. 先离线试玩(SANDBOX,零配置)**
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` 自动下载运行。此时所有数据都在本机沙盒,可放心试 `webaz_register` / `webaz_search` / 下单全流程。
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,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,94 @@
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
+ * 现状:本模块所有 rail 的 legal_cleared=false / allowlist=[] / policy_version=占位 / production_ready=false,
14
+ * 且 Lock A 也全拒 → isBondRailClearedForProduction 恒 false → confirmProductionReceipt 恒抛 →
15
+ * sellerHasProductionBaseBondLocked 恒 false → Direct Pay 仍 non-launchable / fail-closed。
16
+ * 置真值(legal_cleared / 真实 allowlist / 真实 policy_version / production_ready)= 法务清门 + 外审 gated 决定,【不在本 PR】。
17
+ */
18
+ import { getDepositRail } from './deposit-rails.js';
19
+ /** policy_version 占位值(= 未设)。等于此值即视为未放行。 */
20
+ export const BOND_POLICY_VERSION_PLACEHOLDER = 'pre-legal-unset';
21
+ /**
22
+ * 生产 base-bond rail 放行 registry。【显式 switch / 常量,无注册框架】(anti-YAGNI,镜像 deposit-rails.ts)。
23
+ * 只登记【生产收款轨】(usdc_onchain / fiat_psp);manual = 非生产确认轨,不在此(查询返回 null → fail-closed)。
24
+ * 全部默认 fail-closed —— 真值由后续【法务清门 + 外审】PR 配置,不在本 scaffold。
25
+ */
26
+ const BOND_RAIL_CLEARANCE = {
27
+ usdc_onchain: { railId: 'usdc_onchain', assetCategory: 'usdc', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
28
+ fiat_psp: { railId: 'fiat_psp', assetCategory: 'fiat', legalCleared: false, jurisdictionAllowlist: [], policyVersion: BOND_POLICY_VERSION_PLACEHOLDER, productionReady: false },
29
+ };
30
+ /** 取某 rail 的放行记录;未登记(如 manual / 未知)→ null(fail-closed)。 */
31
+ export function getBondRailClearance(railId) {
32
+ return BOND_RAIL_CLEARANCE[railId] ?? null;
33
+ }
34
+ /**
35
+ * 该 rail 在该 jurisdiction 下是否【可用于生产 base-bond】= Lock A(真实实现)且 Lock B(registry 放行)全过。
36
+ * 当前恒 false。纯读,无副作用。
37
+ */
38
+ export function isBondRailClearedForProduction(railId, jurisdiction) {
39
+ const c = getBondRailClearance(railId);
40
+ if (!c)
41
+ return false;
42
+ // Lock A:已实现的生产收款轨(deposit-rails)。与 assertProductionDepositRail 一致用 implemented(非 legalCleared)——
43
+ // legal/治理放行全归 Lock B(本函数下半段 registry)。manual=非生产;usdc/fiat=GATED(implemented=false)。
44
+ let rail;
45
+ try {
46
+ rail = getDepositRail(railId);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ if (!rail.isProduction || !rail.implemented)
52
+ return false;
53
+ // Lock B:registry 放行(全字段)。
54
+ return c.legalCleared && c.productionReady
55
+ && c.policyVersion !== BOND_POLICY_VERSION_PLACEHOLDER && c.policyVersion.length > 0
56
+ && typeof jurisdiction === 'string' && jurisdiction.length > 0
57
+ && c.jurisdictionAllowlist.includes(jurisdiction);
58
+ }
59
+ /**
60
+ * 生产 base-bond 放行【硬闸】(Lock B 入口):未完全放行即抛。confirmProductionReceipt 在 assertProductionDepositRail
61
+ * (Lock A)之后【额外】调用本闸 —— 两把独立锁都过才可能写 production receipt。当前恒抛。
62
+ */
63
+ export function assertBondRailCleared(railId, jurisdiction) {
64
+ if (!isBondRailClearedForProduction(railId, jurisdiction)) {
65
+ 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`);
66
+ }
67
+ }
68
+ /**
69
+ * 只读 readiness/blockers —— 列出某 rail 距离生产放行还差什么(供后续 Phase 7 readiness 面 / 运维诊断,不带 UI/endpoint)。
70
+ * opts.hasProductionReceipt:某具体 deposit 是否已有 production_receipt_confirmed_at(per-deposit,调用方提供事实)。
71
+ * 当前任何 rail 都至少含 RAIL_IMPLEMENTATION_GATED / NO_LEGAL_CLEARED_RAIL / EMPTY_JURISDICTION_ALLOWLIST / POLICY_VERSION_UNSET。
72
+ */
73
+ export function bondRailClearanceBlockers(railId, opts) {
74
+ const out = [];
75
+ let rail = null;
76
+ try {
77
+ rail = getDepositRail(railId);
78
+ }
79
+ catch {
80
+ rail = null;
81
+ }
82
+ if (!rail || !rail.isProduction || !rail.implemented)
83
+ out.push('RAIL_IMPLEMENTATION_GATED');
84
+ const c = getBondRailClearance(railId);
85
+ if (!c || !c.legalCleared || !c.productionReady)
86
+ out.push('NO_LEGAL_CLEARED_RAIL');
87
+ if (!c || c.jurisdictionAllowlist.length === 0)
88
+ out.push('EMPTY_JURISDICTION_ALLOWLIST');
89
+ if (!c || c.policyVersion === BOND_POLICY_VERSION_PLACEHOLDER || c.policyVersion.length === 0)
90
+ out.push('POLICY_VERSION_UNSET');
91
+ if (opts?.hasProductionReceipt !== true)
92
+ out.push('NO_PRODUCTION_RECEIPT');
93
+ return out;
94
+ }
@@ -0,0 +1,145 @@
1
+ import { randomUUID, createHash } from 'crypto';
2
+ // ── allowlists(未知值 fail-closed)──
3
+ const KYB_STATUSES = new Set(['pending', 'approved', 'rejected', 'revoked']);
4
+ const SANCTIONS_STATUSES = new Set(['clear', 'flagged', 'blocked']);
5
+ const AML_RULES = new Set(['structuring', 'concentration', 'cumulative', 'crypto', 'velocity']);
6
+ const AML_SEVERITIES = new Set(['low', 'medium', 'high']);
7
+ const AML_STATUSES = new Set(['open', 'reviewing', 'cleared', 'escalated', 'str_filed']);
8
+ // expires_at 形态:reader 用 `expires_at > datetime('now')` 做【字符串比较】,datetime('now') = 'YYYY-MM-DD HH:MM:SS'。
9
+ // 任意字符串(如 'not-a-date')会被字典序当成"未来"→ 错误地通过 fail-closed reader。故 ingress 必须校验+规范化:
10
+ // 只接受 SQLite datetime 'YYYY-MM-DD HH:MM:SS' 或完整 ISO-8601,且能真实解析;ISO 一律规范化为可比的 SQLite 格式。
11
+ // SQLite UTC civil 形态 'YYYY-MM-DD HH:MM:SS';ISO 形态【强制带 timezone】(Z 或 ±hh:mm)——无 tz 会按服务器本地时区归一,拒。
12
+ const SQLITE_DT_RE = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
13
+ const ISO_DT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
14
+ /**
15
+ * 逐字段校验【真实存在】的 civil datetime —— 用 Date.UTC 重建后逐字段回比,杜绝 JS 自动滚动
16
+ * (如 2099-02-31 → 03-03、25:00 → 次日)。把字段当 UTC 校验真实性即可(与 SQLite datetime('now') UTC 语义一致)。
17
+ */
18
+ function isRealCivil(y, mo, d, h, mi, s) {
19
+ const t = Date.UTC(y, mo - 1, d, h, mi, s);
20
+ if (!Number.isFinite(t))
21
+ return false;
22
+ const dt = new Date(t);
23
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === mo - 1 && dt.getUTCDate() === d
24
+ && dt.getUTCHours() === h && dt.getUTCMinutes() === mi && dt.getUTCSeconds() === s;
25
+ }
26
+ /**
27
+ * 校验+规范化 expiresAt:空→null(无期限,允许);合法→可比的 'YYYY-MM-DD HH:MM:SS'(UTC);非法→ false。
28
+ * 不依赖 Date.parse 的宽松解析:先正则定形、再逐字段 isRealCivil 校验(拒不存在的日期),ISO 必须显式带 tz。
29
+ */
30
+ export function normalizeExpiry(expiresAt) {
31
+ if (expiresAt === undefined || expiresAt === null || expiresAt === '')
32
+ return { ok: true, value: null };
33
+ const sm = SQLITE_DT_RE.exec(expiresAt);
34
+ if (sm) {
35
+ if (!isRealCivil(+sm[1], +sm[2], +sm[3], +sm[4], +sm[5], +sm[6]))
36
+ return { ok: false };
37
+ return { ok: true, value: expiresAt }; // 已是可比的 UTC civil 形态
38
+ }
39
+ const im = ISO_DT_RE.exec(expiresAt);
40
+ if (im) {
41
+ // 先逐字段校验【字面 civil 日期】真实存在(2099-02-31 此处即被拒,与 tz 无关)。
42
+ if (!isRealCivil(+im[1], +im[2], +im[3], +im[4], +im[5], +im[6]))
43
+ return { ok: false };
44
+ const ms = Date.parse(expiresAt); // 字面已合法 + 强制带 tz → 此处只做 tz 偏移换算得真实 UTC instant
45
+ if (!Number.isFinite(ms))
46
+ return { ok: false };
47
+ return { ok: true, value: new Date(ms).toISOString().slice(0, 19).replace('T', ' ') };
48
+ }
49
+ return { ok: false };
50
+ }
51
+ /** providerRef → 审计用短引用(sha256 前 16 hex);无则 undefined。原值不进审计明文。 */
52
+ function providerRefHash(providerRef) {
53
+ if (!providerRef)
54
+ return undefined;
55
+ return createHash('sha256').update(providerRef).digest('hex').slice(0, 16);
56
+ }
57
+ /**
58
+ * AML detail allowlist key 集 —— 与 #108 monitor 实际写入的聚合字段一致(velocity/concentration)。
59
+ * key 也走 allowlist(不止 value):杜绝把 PII 藏进 key(如 {"alice@example.com":1} / {"wallet_0x..":1})。
60
+ */
61
+ export const AML_DETAIL_KEYS = new Set(['window_hours', 'order_count', 'threshold', 'small_order_count', 'small_order_amount']);
62
+ /**
63
+ * AML detail = 仅【allowlist key + 聚合数字 value】(与 #108 monitor 纪律一致,防 PII 落库:
64
+ * 邮箱/地址/钱包/叙述性笔记无论藏在 value 还是 key 一律拒)。undefined 视为"无 detail"(合法)。
65
+ */
66
+ export function isNumericDetail(detail) {
67
+ if (detail === undefined || detail === null)
68
+ return true;
69
+ if (typeof detail !== 'object' || Array.isArray(detail))
70
+ return false;
71
+ return Object.entries(detail).every(([k, v]) => AML_DETAIL_KEYS.has(k) && typeof v === 'number' && Number.isFinite(v));
72
+ }
73
+ /**
74
+ * AML detail 的 canonical hash(key 排序后 JSON → sha256 前 16 hex),供 Passkey purpose_data 绑定【写入内容】用
75
+ * (route 与签名方用同一函数,保证"签的 detail = 写的 detail")。无 detail → 固定 'none'。
76
+ */
77
+ export function amlDetailHash(detail) {
78
+ if (detail === undefined || detail === null)
79
+ return 'none';
80
+ const canonical = JSON.stringify(Object.keys(detail).sort().reduce((o, k) => { o[k] = detail[k]; return o; }, {}));
81
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
82
+ }
83
+ /** 审计写入(append-only;PII-free)。与台账 INSERT 同事务调用。 */
84
+ function writeAudit(db, adminId, action, targetType, targetId, detail) {
85
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
86
+ .run('cing_' + randomUUID(), adminId, action, targetType, targetId, JSON.stringify(detail));
87
+ }
88
+ /** KYB 复核结论 ingress(append-only)。status ∈ {pending,approved,rejected,revoked}。 */
89
+ export function recordKybReview(db, args) {
90
+ const { userId, reviewerId, status, providerRef, expiresAt } = args;
91
+ if (!userId || !reviewerId)
92
+ return { ok: false, error: 'MISSING_ARGS' };
93
+ if (!KYB_STATUSES.has(status))
94
+ return { ok: false, error: 'INVALID_STATUS' };
95
+ const exp = normalizeExpiry(expiresAt);
96
+ if (!exp.ok)
97
+ return { ok: false, error: 'INVALID_EXPIRES_AT' };
98
+ const id = 'kyb_' + randomUUID();
99
+ db.transaction(() => {
100
+ db.prepare(`INSERT INTO direct_receive_kyb_reviews (id, user_id, status, reviewed_by, reviewed_at, expires_at, reason)
101
+ VALUES (?,?,?,?,datetime('now'),?,?)`).run(id, userId, status, reviewerId, exp.value, providerRef ?? null);
102
+ writeAudit(db, reviewerId, 'direct_pay.kyb_ingress', 'user', userId, { kyb_status: status, provider_ref_hash: providerRefHash(providerRef), has_expiry: !!expiresAt, review_id: id });
103
+ })();
104
+ return { ok: true, id };
105
+ }
106
+ /** 制裁筛查结论 ingress(append-only)。status ∈ {clear,flagged,blocked}。 */
107
+ export function recordSanctionsScreening(db, args) {
108
+ const { userId, reviewerId, status, providerRef, expiresAt } = args;
109
+ if (!userId || !reviewerId)
110
+ return { ok: false, error: 'MISSING_ARGS' };
111
+ if (!SANCTIONS_STATUSES.has(status))
112
+ return { ok: false, error: 'INVALID_STATUS' };
113
+ const exp = normalizeExpiry(expiresAt);
114
+ if (!exp.ok)
115
+ return { ok: false, error: 'INVALID_EXPIRES_AT' };
116
+ const id = 'sc_' + randomUUID();
117
+ db.transaction(() => {
118
+ db.prepare(`INSERT INTO sanctions_screening (id, user_id, status, source, reason, screened_at, expires_at)
119
+ VALUES (?,?,?,?,?,datetime('now'),?)`).run(id, userId, status, 'manual_ingress', providerRef ?? null, exp.value);
120
+ writeAudit(db, reviewerId, 'direct_pay.sanctions_ingress', 'user', userId, { sanctions_status: status, provider_ref_hash: providerRefHash(providerRef), has_expiry: !!expiresAt, screening_id: id });
121
+ })();
122
+ return { ok: true, id };
123
+ }
124
+ /** AML flag ingress(append-only,新建 flag)。rule/severity/status 全 allowlist。 */
125
+ export function recordAmlFlagIngress(db, args) {
126
+ const { userId, reviewerId, rule, severity, status, relatedOrderId, detail } = args;
127
+ if (!userId || !reviewerId)
128
+ return { ok: false, error: 'MISSING_ARGS' };
129
+ if (!AML_RULES.has(rule))
130
+ return { ok: false, error: 'INVALID_RULE' };
131
+ if (!AML_SEVERITIES.has(severity))
132
+ return { ok: false, error: 'INVALID_SEVERITY' };
133
+ if (!AML_STATUSES.has(status))
134
+ return { ok: false, error: 'INVALID_STATUS' };
135
+ // P2: detail 仅允许聚合数字(防 PII 落库)。非纯数字 → fail-closed,不写。
136
+ if (!isNumericDetail(detail))
137
+ return { ok: false, error: 'INVALID_DETAIL' };
138
+ const id = 'amlf_' + randomUUID();
139
+ db.transaction(() => {
140
+ db.prepare(`INSERT INTO aml_flags (id, subject_user_id, related_order_id, rule, severity, detail, status)
141
+ VALUES (?,?,?,?,?,?,?)`).run(id, userId, relatedOrderId ?? null, rule, severity, detail ? JSON.stringify(detail) : null, status);
142
+ writeAudit(db, reviewerId, 'direct_pay.aml_ingress', 'aml_flag', id, { subject_user_id: userId, rule, severity, aml_status: status, related_order_id: relatedOrderId ?? null });
143
+ })();
144
+ return { ok: true, id };
145
+ }
@@ -0,0 +1,136 @@
1
+ // PR-6D: 把 #108 的 AML 监控 param 描述(默认 inert)经控制面统一再导出,使 server.ts DEFAULT_PARAMS
2
+ // 可在【既有 import 行】上一并取用(server.ts 已到 LOC 上限,避免新增行)。SSOT 仍在 direct-pay-aml-monitor.ts。
3
+ export { DIRECT_PAY_AML_PARAMS } from './direct-pay-aml-monitor.js';
4
+ // ── buyer-facing de-identification(SSOT)─────────────────────────────────────────────────────────
5
+ // 买家边界脱敏:卖家【私密类】拒因(暂停 / 保证金未交 / KYC·制裁 / AML / 缓交额度)一律收敛为单一
6
+ // DIRECT_PAY_SELLER_NOT_ELIGIBLE,绝不向买家暴露卖家具体合规/额度状态。全局/运营类(DISABLED / RAIL_BREAKER /
7
+ // REGION / CAP)非敏感,原样透出。create 与 availability 两个买家面端点【共用】本判定,避免 de-id 集合漂移。
8
+ // 精确 code 仍保留在 helper 返回值 + 单测(controls / deferral-quota)层,供运营/调试与 gate 逻辑验证。
9
+ // 缓交额度码须与 direct-pay-deferral-quota.ts 的 DEFERRAL_QUOTA_CODES 对齐(test-direct-pay-deferral-quota 漂移断言守护)。
10
+ export const DIRECT_PAY_SELLER_NOT_ELIGIBLE = 'DIRECT_PAY_SELLER_NOT_ELIGIBLE';
11
+ export const BUYER_FACING_SELLER_PRIVATE_CODES = new Set([
12
+ 'DIRECT_PAY_SELLER_SUSPENDED', 'DIRECT_PAY_NOT_AVAILABLE', 'DIRECT_PAY_KYC_REQUIRED', 'DIRECT_PAY_AML_REVIEW_REQUIRED',
13
+ 'DIRECT_PAY_DEFERRAL_QUOTA_EXCEEDED', 'DIRECT_PAY_DEFERRAL_AMOUNT_EXCEEDED',
14
+ 'EXPOSURE_CAP_EXCEEDED', 'EXPOSURE_CAP_CONFIG', // §6.5 抵押背书敞口上限:卖家私密风险态,不向买家泄露
15
+ 'FEE_PREPAY_INSUFFICIENT', // 平台服务费预充值余额不足(非首单):卖家私密风险态,不向买家泄露
16
+ ]);
17
+ /** 买家面脱敏:私密拒因 → 通用 SELLER_NOT_ELIGIBLE;全局/运营类原样;undefined → 通用(fail-safe,绝不泄露)。 */
18
+ export function coarsenBuyerFacingDirectPayCode(code) {
19
+ return !code || BUYER_FACING_SELLER_PRIVATE_CODES.has(code) ? DIRECT_PAY_SELLER_NOT_ELIGIBLE : code;
20
+ }
21
+ export const DEFAULT_DIRECT_PAY_CONTROLS = {
22
+ enabled: false, railBreakerTripped: false, region: '', regionAllowlist: [], perTxCapUnits: 0,
23
+ };
24
+ const isNonNegUnits = (x) => typeof x === 'number' && Number.isSafeInteger(x) && x >= 0;
25
+ /**
26
+ * 入口控制判定(纯、fail-closed、total)。顺序(便宜/广 → 卖家专属 → 硬不变量):
27
+ * 全局开关 → 运营熔断 → 地区 → 单笔上限 → 卖家熔断 → production base-bond → KYC/制裁 → AML 断路器。
28
+ * 任一不过即返回该项拒绝码(短路;先全局/政策、后卖家专属,避免提前泄露卖家专属原因);全过返回 { ok:true }。绝不抛错。
29
+ * production base-bond、KYC/制裁、AML 断路器都是【不可配置硬不变量】(无 cfg 开关、治理不可绕过),恒在最后强制。
30
+ */
31
+ export function evaluateDirectPayLaunchControls(cfg, facts) {
32
+ const c = { ...DEFAULT_DIRECT_PAY_CONTROLS, ...(cfg ?? {}) };
33
+ const f = facts ?? {};
34
+ const deny = (error_code, reason) => ({ ok: false, status: 409, error_code, reason });
35
+ if (c.enabled !== true)
36
+ return deny('DIRECT_PAY_DISABLED', '直付当前未开放(全局开关关闭)');
37
+ if (c.railBreakerTripped === true)
38
+ return deny('DIRECT_PAY_RAIL_BREAKER', '直付已被运营紧急熔断,暂停受理');
39
+ const allow = Array.isArray(c.regionAllowlist) ? c.regionAllowlist : [];
40
+ if (!allow.length || !c.region || !allow.includes(c.region))
41
+ return deny('DIRECT_PAY_REGION_UNSUPPORTED', '直付在本地区暂未开放');
42
+ if (!isNonNegUnits(c.perTxCapUnits) || c.perTxCapUnits <= 0)
43
+ return deny('DIRECT_PAY_CAP_EXCEEDED', '直付单笔上限未配置');
44
+ if (!isNonNegUnits(f.amountUnits) || f.amountUnits <= 0 || f.amountUnits > c.perTxCapUnits)
45
+ return deny('DIRECT_PAY_CAP_EXCEEDED', '直付订单总额超出单笔上限(WebAZ 记录的订单金额上限;不约束场外实付)');
46
+ if (f.sellerBreakerTripped === true)
47
+ return deny('DIRECT_PAY_SELLER_SUSPENDED', '该卖家直付已被暂停');
48
+ // 硬不变量(launch blockers):production base-bond 与 KYC/制裁【始终强制】,无 cfg 开关、治理不可绕过。
49
+ if (f.baseBondSatisfied !== true)
50
+ return deny('DIRECT_PAY_NOT_AVAILABLE', '直付暂不可用:卖家未交履约保证金且无有效缓交');
51
+ if (f.kycSanctionsPassed !== true)
52
+ return deny('DIRECT_PAY_KYC_REQUIRED', '直付暂不可用:卖家未通过 KYC/制裁筛查');
53
+ if (f.amlClear !== true)
54
+ return deny('DIRECT_PAY_AML_REVIEW_REQUIRED', '直付暂不可用:卖家存在未清除的 AML 风险复核');
55
+ return { ok: true, status: 200 };
56
+ }
57
+ /** protocol_params 薄装配器(治理可调;缺行回落 fail-closed 默认)。不读资金/状态,只读控制参数。 */
58
+ export function readDirectPayControlsConfig(getProtocolParam) {
59
+ const csv = String(getProtocolParam('direct_pay.region_allowlist', '') || '');
60
+ const cap = Number(getProtocolParam('direct_pay.per_tx_cap_units', 0));
61
+ return {
62
+ enabled: getProtocolParam('direct_pay.enabled', false) === true,
63
+ railBreakerTripped: getProtocolParam('direct_pay.rail_breaker_tripped', false) === true,
64
+ region: String(getProtocolParam('direct_pay.region', '') || ''),
65
+ regionAllowlist: csv.split(',').map(s => s.trim()).filter(Boolean),
66
+ perTxCapUnits: Number.isSafeInteger(cap) && cap >= 0 ? cap : 0,
67
+ };
68
+ }
69
+ /**
70
+ * protocol_params 默认 seed(供 server.ts DEFAULT_PARAMS 展开)。默认全 fail-closed —— Direct Pay non-launchable;
71
+ * 治理经 PATCH /api/admin/protocol-params/<key> 开通(无 seed 则 PATCH 404、boot 不建行 → 控制面打不开)。
72
+ * 只含【运营节流】可调项(开关/地区/上限)。production base-bond 与 KYC/制裁是【不可关闭的硬不变量】,
73
+ * 刻意【不】做成 param —— 治理无法关掉它们绕过 launch blockers(evaluate 始终强制)。
74
+ * key/默认值必须与 readDirectPayControlsConfig / DEFAULT_DIRECT_PAY_CONTROLS 对齐。
75
+ */
76
+ export const DIRECT_PAY_CONTROL_PARAMS = [
77
+ { key: 'direct_pay.enabled', value: 'false', type: 'boolean', description: 'Direct Pay 全局主开关(上线决定);默认 false=关(non-launchable)。', category: 'system' },
78
+ { key: 'direct_pay.rail_breaker_tripped', value: 'false', type: 'boolean', description: 'Direct Pay 运营紧急熔断(ops emergency stop,与 enabled 分离);true=暂停受理。默认 false。', category: 'system' },
79
+ { key: 'direct_pay.region', value: '', type: 'string', description: 'Direct Pay 本部署/运营所在地区码(与白名单比对);默认空=fail-closed。', category: 'system' },
80
+ { key: 'direct_pay.region_allowlist', value: '', type: 'string', description: 'Direct Pay 已开放地区白名单(逗号分隔);默认空=无地区开放。', category: 'system' },
81
+ // 单笔上限:对【WebAZ 记录的 direct_p2p 订单总额】在建单时的天花板(整数 policy base-units)。默认 0 = 无放行(fail-closed),
82
+ // 治理设正值方生效。【不是】对买卖双方场外真实付款金额的担保或控制——WebAZ 控不了场外金额,此处也不声称能控。
83
+ // 具体数值(如 SG v1 的 policy units)由独立的 launch-policy PR 配置,本 PR 只提供 cap 能力、默认仍 fail-closed。
84
+ { key: 'direct_pay.per_tx_cap_units', value: '0', type: 'number', description: 'Direct Pay 单笔上限:WebAZ 记录的订单总额天花板(整数 policy base-units);默认 0=无放行,治理设正值方可。仅约束协议侧建单金额,不控制/不担保场外真实付款。', category: 'system', min: 0 },
85
+ { key: 'direct_pay.exposure_factor_bps', value: '8000', type: 'number', description: '§6.5 抵押背书的开放敞口上限系数(bps):open_exposure + new_order ≤ active_collateral × bps/10000。仅对有真实链上抵押(collateral>0)的卖家生效;缓交卖家不受此门。默认 8000(=80%)。风险控制,非买家赔付。', category: 'system', min: 1, max: 10000 },
86
+ // 注:平台服务费门 = 首单宽限 + 预充值续用(数据驱动:available_prepay = Σ预充值 − Σ已计提费),【无 protocol_param 旋钮】
87
+ // (额度即商家实际预付余额,宽限自动判定);故此处不再有 fee_ar_credit_ceiling_units 参数。
88
+ ];
89
+ /**
90
+ * PR-6A AML/CFT runtime — KYB(商户尽调)事实(fail-closed)。来源 direct_receive_kyb_reviews(真人/合规复核结论;
91
+ * 无第三方 vendor、无真实 API 调用)。通过 ⟺ 存在 status='approved' 且未过期的复核 且【无】rejected/revoked 行。
92
+ * missing / pending / rejected / revoked / expired 一律不通过。该表无生产写入方 → 真实卖家天然 fail-closed。
93
+ */
94
+ export function sellerDirectPayKybPassed(db, sellerId) {
95
+ const approved = db.prepare("SELECT 1 FROM direct_receive_kyb_reviews WHERE user_id = ? AND status = 'approved' AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1").get(sellerId);
96
+ const blocked = db.prepare("SELECT 1 FROM direct_receive_kyb_reviews WHERE user_id = ? AND status IN ('rejected','revoked') LIMIT 1").get(sellerId);
97
+ return !!approved && !blocked;
98
+ }
99
+ /**
100
+ * PR-6A AML/CFT runtime — 制裁筛查事实(fail-closed)。来源 sanctions_screening:存在 status='clear' 且未过期的结论,
101
+ * 且【无】flagged/blocked 行。expired(expires_at 已过)视作未通过;NULL expires_at = 无期限。无第三方集成。
102
+ * 该表无生产写入方 → 真实卖家天然 fail-closed。
103
+ */
104
+ export function sellerDirectPaySanctionsClear(db, sellerId) {
105
+ const clear = db.prepare("SELECT 1 FROM sanctions_screening WHERE user_id = ? AND status = 'clear' AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1").get(sellerId);
106
+ const bad = db.prepare("SELECT 1 FROM sanctions_screening WHERE user_id = ? AND status IN ('flagged','blocked') LIMIT 1").get(sellerId);
107
+ return !!clear && !bad;
108
+ }
109
+ /**
110
+ * 卖家熔断事实(per-seller breaker)。【唯一来源】= direct_receive_privileges.status = 'suspended'(复用既有暂停语义,
111
+ * 如 slashBond 罚没后置 suspended);【不】新增第二套 suspension 概念。无行 / 'none' / 'active' → false;'suspended' → true。
112
+ */
113
+ export function sellerDirectPayBreakerTripped(db, sellerId) {
114
+ return !!db.prepare("SELECT 1 FROM direct_receive_privileges WHERE user_id = ? AND status = 'suspended' LIMIT 1").get(sellerId);
115
+ }
116
+ /**
117
+ * PR-6B AML 运行期断路器事实(fail-closed)。来源 aml_flags(本仓内部监控 flag;无第三方 vendor、无真实 API)。
118
+ * key = subject_user_id。返回 true(清白/不阻断)⟺ 卖家【没有任何阻断性 flag】。
119
+ *
120
+ * 单条 flag 的【阻断】判定(任一命中即阻断;precedence 由 SQL OR 短路保证 suspend 优先于 cleared):
121
+ * ① malformed enum —— severity/status/disposition 越界(本表 TEXT 无 CHECK,脏值可入)→ fail-closed 阻断;
122
+ * ② disposition='suspend' —— 无论 severity/status【一律阻断】(含 status='cleared' 的矛盾数据:suspend 取胜,从严);
123
+ * ③ status∈(open|reviewing|escalated|str_filed) 且 severity∈(medium|high) —— 未清除的中/高风险 → 阻断。
124
+ * 非阻断(放行):无 flag / status='cleared'(且非 suspend) / severity='low'(且非 suspend)。
125
+ * 纯读;不写;不碰资金/状态机。aml_flags 当前无生产写入方 → 真实卖家天然无阻断(待 AML 检测引擎接入再产生 flag)。
126
+ */
127
+ export function sellerDirectPayAmlClear(db, sellerId) {
128
+ const blocking = db.prepare(`SELECT 1 FROM aml_flags WHERE subject_user_id = ? AND (
129
+ severity NOT IN ('low','medium','high')
130
+ OR status NOT IN ('open','reviewing','cleared','escalated','str_filed')
131
+ OR (disposition IS NOT NULL AND disposition NOT IN ('review_queue','downgrade','suspend'))
132
+ OR disposition = 'suspend'
133
+ OR (status IN ('open','reviewing','escalated','str_filed') AND severity IN ('medium','high'))
134
+ ) LIMIT 1`).get(sellerId);
135
+ return !blocking;
136
+ }