@seasonkoh/webaz 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +4 -3
  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 +80 -0
  31. package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
  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-account.js +977 -0
  46. package/dist/pwa/public/app-admin.js +608 -0
  47. package/dist/pwa/public/app-agents.js +63 -0
  48. package/dist/pwa/public/app-ai.js +2162 -0
  49. package/dist/pwa/public/app-chat-poll.js +29 -0
  50. package/dist/pwa/public/app-contribution.js +836 -0
  51. package/dist/pwa/public/app-create-kinds.js +17 -0
  52. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  53. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  54. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  55. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  56. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  57. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  58. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  59. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  60. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  61. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  62. package/dist/pwa/public/app-direct-pay.js +227 -0
  63. package/dist/pwa/public/app-discover.js +1296 -0
  64. package/dist/pwa/public/app-external-links.js +32 -0
  65. package/dist/pwa/public/app-listings.js +226 -0
  66. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  67. package/dist/pwa/public/app-price.js +55 -0
  68. package/dist/pwa/public/app-product-media.js +15 -0
  69. package/dist/pwa/public/app-profile.js +1692 -0
  70. package/dist/pwa/public/app-seller.js +199 -0
  71. package/dist/pwa/public/app-shop.js +1145 -0
  72. package/dist/pwa/public/app.js +13388 -22277
  73. package/dist/pwa/public/i18n.js +413 -196
  74. package/dist/pwa/public/index.html +27 -0
  75. package/dist/pwa/public/openapi.json +579 -1
  76. package/dist/pwa/routes/admin-analytics.js +4 -2
  77. package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
  78. package/dist/pwa/routes/agent-grants.js +255 -0
  79. package/dist/pwa/routes/buyer-feeds.js +2 -1
  80. package/dist/pwa/routes/chat.js +6 -1
  81. package/dist/pwa/routes/dashboards.js +2 -1
  82. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  83. package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
  84. package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
  85. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  86. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  87. package/dist/pwa/routes/fx.js +12 -0
  88. package/dist/pwa/routes/leaderboard.js +47 -9
  89. package/dist/pwa/routes/listings.js +2 -2
  90. package/dist/pwa/routes/logistics.js +4 -0
  91. package/dist/pwa/routes/manifests.js +38 -0
  92. package/dist/pwa/routes/me-data.js +5 -2
  93. package/dist/pwa/routes/orders-action.js +117 -9
  94. package/dist/pwa/routes/orders-create.js +4 -0
  95. package/dist/pwa/routes/orders-read.js +36 -0
  96. package/dist/pwa/routes/p2p-products.js +2 -2
  97. package/dist/pwa/routes/products-create.js +5 -3
  98. package/dist/pwa/routes/products-links.js +34 -0
  99. package/dist/pwa/routes/products-list.js +2 -1
  100. package/dist/pwa/routes/products-update.js +22 -2
  101. package/dist/pwa/routes/promoter.js +3 -0
  102. package/dist/pwa/routes/referral.js +4 -0
  103. package/dist/pwa/routes/returns.js +26 -1
  104. package/dist/pwa/routes/rewards-apply.js +10 -5
  105. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  106. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  107. package/dist/pwa/routes/shops.js +2 -1
  108. package/dist/pwa/routes/url-claim.js +2 -2
  109. package/dist/pwa/routes/users-public.js +8 -0
  110. package/dist/pwa/routes/wallet-read.js +3 -0
  111. package/dist/pwa/routes/webauthn.js +7 -2
  112. package/dist/pwa/server-schema.js +9 -0
  113. package/dist/pwa/server.js +261 -1706
  114. package/dist/runtime/agent-grant-scopes.js +128 -0
  115. package/dist/runtime/agent-grant-verifier.js +67 -0
  116. package/dist/runtime/agent-pairing.js +60 -0
  117. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  118. package/dist/runtime/webaz-schema-helpers.js +1952 -0
  119. package/dist/store-verification.js +77 -0
  120. package/dist/version.js +1 -1
  121. package/package.json +80 -2
@@ -13,8 +13,8 @@
13
13
  * webaz_wallet 查看钱包余额
14
14
  * …(39 工具,完整定义见下方 TOOLS 数组;数量以 TOOLS.length 为准)
15
15
  *
16
- * 双模(RFC-003):NETWORK(WEBAZ_API_KEY → 调 webaz.xyz)/ SANDBOX(本机库);见 NETWORK_TOOLS / apiCall / toolBackend。
17
- * 关联 / Related: AGENTS.md · RFC-003(双模) · RFC-004(webaz_feedback) · 元规则 #4 不撒谎(_mode 戳) /
16
+ * 三态(RFC-003):NETWORK(WEBAZ_API_KEY → 调 webaz.xyz)/ NETWORK 只读(无 key,公共读走 webaz.xyz)/ SANDBOX(仅 WEBAZ_MODE=sandbox,本机库);见 NETWORK_TOOLS / apiCall / toolBackend。
17
+ * 关联 / Related: AGENTS.md · RFC-003(三态:network / network_readonly / sandbox) · RFC-004(webaz_feedback) · 元规则 #4 不撒谎(_mode 戳) /
18
18
  * #6 不滥用(agent 责任制 + Iron-Rule 真人动作)· 生产端点在 src/pwa(NETWORK 共用)
19
19
  */
20
20
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -23,6 +23,13 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
23
23
  GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
24
24
  import { initDatabase, generateId } from '../../layer0-foundation/L0-1-database/schema.js';
25
25
  import { setSeamDb } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam(本进程注入)
26
+ import { applyWebazRuntimeSchema } from '../../runtime/apply-webaz-runtime-schema.js'; // 与 PWA 同源的纯 schema 桥(防 MCP fresh DB 漂移)
27
+ import { generateCodeVerifier, pkceChallengeS256 } from '../../runtime/agent-pairing.js'; // RFC-020 PR-C1 — PKCE 配对
28
+ import { NETWORK_TOOLS, NETWORK_SELF_AWARE, toolAllowedInNetworkMode, resolveMode } from './network-mode.js'; // RFC-003 网络门(可单测)+ 模式解析(单一真相源)
29
+ import { homedir } from 'node:os';
30
+ import { join as pathJoin } from 'node:path';
31
+ import { existsSync as fsExists, mkdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync } from 'node:fs';
32
+ import { execFileSync } from 'node:child_process';
26
33
  import { transition, getOrderStatus, initSystemUser, } from '../../layer0-foundation/L0-2-state-machine/engine.js';
27
34
  import { initDisputeSchema, createDispute, respondToDispute, getDisputeDetails, getOrderDispute, getOpenDisputes, } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
28
35
  import { initNotificationSchema, notifyTransition, getNotifications, getUnreadCount, markRead, } from '../../layer2-business/L2-6-notifications/notification-engine.js';
@@ -38,7 +45,7 @@ const TELEMETRY_URL = process.env.WEBAZ_TELEMETRY_URL ?? 'https://webaz.xyz/api/
38
45
  // 2026-06-01: phase A pre-launch 默认 OFF(opt-in)— W8 public launch 时翻回 default ON + 加 README 披露段
39
46
  // Phase A pre-launch: telemetry default OFF (opt-in). Flip to default ON at W8 launch + add README disclosure section.
40
47
  const TELEMETRY_ENABLED = (process.env.WEBAZ_TELEMETRY ?? 'off').toLowerCase() === 'on';
41
- // ─── RFC-003 P0: 双模(NETWORK / SANDBOX)骨架 ─────────────────────
48
+ // ─── RFC-003 P0: 三态(network_readonly / network / sandbox)骨架 ─────────────────────
42
49
  // NETWORK = 带 api_key 调 webaz.xyz/api(加入共享生产网络);SANDBOX = 本地 SQLite(离线试玩,与全网隔离)。
43
50
  // P0 不迁移任何工具(NETWORK_TOOLS 为空)→ 一切仍走本地 = 零行为变化;P1/P2 逐个把工具名加入集合切到网络。
44
51
  const WEBAZ_API_URL = (process.env.WEBAZ_API_URL ?? 'https://webaz.xyz').replace(/\/+$/, '');
@@ -52,67 +59,14 @@ export function resolveMcpApiKey(args, envKey = WEBAZ_API_KEY) {
52
59
  const explicit = typeof args?.api_key === 'string' ? args.api_key.trim() : '';
53
60
  return explicit || envKey;
54
61
  }
55
- const WEBAZ_MODE_ENV = (process.env.WEBAZ_MODE ?? '').toLowerCase();
56
62
  // 模式:显式 WEBAZ_MODE 优先;否则有 api_key → network,无 key → network_readonly(装完即见真网络)。
57
63
  // network_readonly(L1 onboarding,2026-06-08):无 key 默认。公共读匿名打 webaz.xyz(真 catalog/协议),
58
64
  // 需身份的写/读返回"设 WEBAZ_API_KEY(到 #welcome 申请邀请)"。离线本地 playground 改为【显式】 WEBAZ_MODE=sandbox。
59
65
  // —— 治"装完=空沙盒劝退"的死首体验;route/guard 与 network 同路(见 isNetworkMode),只是无 Bearer + 文案不同。
60
- const MODE = WEBAZ_MODE_ENV === 'network' ? 'network'
61
- : WEBAZ_MODE_ENV === 'sandbox' ? 'sandbox'
62
- : WEBAZ_MODE_ENV === 'network_readonly' ? 'network_readonly'
63
- : (WEBAZ_API_KEY ? 'network' : 'network_readonly');
66
+ // 单一真相源:resolveMode(见 network-mode.ts),CLI --mode/--doctor 用同一函数,杜绝漂移。
67
+ const MODE = resolveMode(process.env);
64
68
  // network 或 network_readonly 都"走真网络"(后者无 Bearer)。sandbox 才是本地。
65
69
  const isNetworkMode = () => MODE === 'network' || MODE === 'network_readonly';
66
- // 已迁移到 NETWORK 的工具名。P1/P2 逐个加入;未在集合里的工具仍走 sandbox(本地)。
67
- // P1(读工具): 纯公开读,无写无 Passkey,作"MCP 连得上生产网络"的首验证。
68
- const NETWORK_TOOLS = new Set([
69
- 'webaz_price_history',
70
- 'webaz_leaderboard',
71
- 'webaz_verify_price',
72
- 'webaz_place_order',
73
- 'webaz_list_product',
74
- 'webaz_update_order',
75
- 'webaz_search',
76
- 'webaz_get_status',
77
- 'webaz_feedback',
78
- 'webaz_contribute',
79
- // Batch 1(只读 + 低危自身写):走 webaz.xyz Bearer api_key。
80
- 'webaz_notifications',
81
- 'webaz_nearby',
82
- 'webaz_profile',
83
- 'webaz_shareables',
84
- 'webaz_mykey',
85
- // Batch 2(低危写,无钱无 escrow):走 webaz.xyz Bearer api_key。
86
- // 注:share_link 暂不迁(无对应服务端端点,需新建,留待后续)。
87
- 'webaz_follows',
88
- 'webaz_like',
89
- 'webaz_blocklist',
90
- 'webaz_default_address',
91
- 'webaz_chat',
92
- 'webaz_rfq',
93
- 'webaz_referral',
94
- // Batch 3(商务):secondhand/skill_market/auction 纯 pwaApi(mode-aware 自动走网络);
95
- // skill 直连本地引擎,加了显式 apiCall network 分支。
96
- 'webaz_secondhand',
97
- 'webaz_skill',
98
- 'webaz_skill_market',
99
- 'webaz_auction',
100
- // Batch 4(资金/质押,守恒由服务端 RFC-014 保证;wallet 只读,写=Passkey 仅 PWA):
101
- 'webaz_wallet',
102
- 'webaz_trial',
103
- 'webaz_charity',
104
- 'webaz_bid',
105
- 'webaz_auto_bid',
106
- // Batch 5(铁律/敏感):claim_verify 纯 pwaApi(真人门由服务端 require_human_presence 强制);
107
- // dispute view/list_open/respond/add_evidence 走网络,arbitrate 仅返回 Passkey 指引;
108
- // rotate_key/revoke_key 仅返回 Passkey 指引(不本地校验)。
109
- 'webaz_dispute',
110
- 'webaz_claim_verify',
111
- 'webaz_rotate_key',
112
- 'webaz_revoke_key',
113
- // #1122:share_link 现有服务端端点 /api/share-link,可走网络。
114
- 'webaz_share_link',
115
- ]);
116
70
  const recentCalls = [];
117
71
  function pushRecentCall(c) {
118
72
  recentCalls.push(c);
@@ -124,9 +78,8 @@ function pushRecentCall(c) {
124
78
  function toolBackend(tool) {
125
79
  return (isNetworkMode() && NETWORK_TOOLS.has(tool)) ? 'network' : 'sandbox';
126
80
  }
127
- // 未在 NETWORK_TOOLS 名单、但 NETWORK 模式下仍可本地运行的"自省/引导"工具(非数据操作)。
128
- // info = 本地自省(并拉 live 网络状态);register = 引导真人去 webaz.xyz。其余未迁工具一律硬失败。
129
- const NETWORK_SELF_AWARE = new Set(['webaz_info', 'webaz_register']);
81
+ // NETWORK_SELF_AWARE imported from ./network-mode.ts (info = local introspection;
82
+ // register = redirect human to webaz.xyz). Everything else un-migrated hard-fails.
130
83
  // RFC-003 Batch 0 安全网:NETWORK 模式下调用【未迁移】工具时的诚实拒绝(而非静默落本地沙盒)。
131
84
  // 否则带 key 的用户调未迁工具会被悄悄喂本地结果——写操作=幻影操作(根本没到 webaz.xyz)。
132
85
  function networkMigrationPending(tool) {
@@ -180,6 +133,200 @@ async function apiCall(path, opts = {}) {
180
133
  return { error: `网络错误:${msg}`, network_error: true };
181
134
  }
182
135
  }
136
+ // ─────────────────────────── RFC-020 PR-C1: webaz_pair (pairing + local credential storage) ───────────────────────────
137
+ // C1 scope: pairing / consent / retrieval / LOCAL credential storage ONLY. The grant is
138
+ // NOT wired into any business tool (that is PR-C2 — per-request scope enforcement + audit).
139
+ // The raw bearer appears exactly once, in the server→MCP retrieval response; it is written
140
+ // straight to the OS secret store (Keychain → 0600 file fallback) and NEVER returned to the
141
+ // user / chat / tool-response / log. webaz_pair returns only a credential handle + status.
142
+ const WEBAZ_DIR = pathJoin(homedir(), '.webaz');
143
+ const PENDING_PATH = pathJoin(WEBAZ_DIR, 'pairing-pending.json'); // holds the PKCE verifier between start→complete
144
+ const CRED_FALLBACK_PATH = pathJoin(WEBAZ_DIR, 'credentials'); // 0600 fallback when Keychain unavailable
145
+ const KEYCHAIN_SERVICE = 'webaz-grant';
146
+ function ensureWebazDir() { if (!fsExists(WEBAZ_DIR))
147
+ mkdirSync(WEBAZ_DIR, { recursive: true, mode: 0o700 }); }
148
+ function write0600(path, data) { ensureWebazDir(); writeFileSync(path, data, { mode: 0o600 }); try {
149
+ chmodSync(path, 0o600);
150
+ }
151
+ catch { /* best effort */ } }
152
+ /** Store the raw bearer in the OS secret store; never log it. Returns an opaque handle. */
153
+ function storeGrantCredential(grantId, token) {
154
+ // macOS Keychain first (non-interactive upsert). Any failure → strict-perms file fallback.
155
+ if (process.platform === 'darwin') {
156
+ try {
157
+ execFileSync('security', ['add-generic-password', '-U', '-s', KEYCHAIN_SERVICE, '-a', grantId, '-w', token], { stdio: ['ignore', 'ignore', 'ignore'] });
158
+ return `keychain:${KEYCHAIN_SERVICE}/${grantId}`;
159
+ }
160
+ catch { /* fall through to file */ }
161
+ }
162
+ let store = {};
163
+ try {
164
+ if (fsExists(CRED_FALLBACK_PATH))
165
+ store = JSON.parse(readFileSync(CRED_FALLBACK_PATH, 'utf8'));
166
+ }
167
+ catch {
168
+ store = {};
169
+ }
170
+ store[grantId] = { token, stored_at: new Date().toISOString() };
171
+ write0600(CRED_FALLBACK_PATH, JSON.stringify(store, null, 2));
172
+ return `file:~/.webaz/credentials#${grantId}`;
173
+ }
174
+ // Non-secret index of the current grant (grant_id + handle + metadata) so the stored
175
+ // credential can be resolved back without re-pairing. The SECRET (token) lives only in
176
+ // the OS secret store / 0600 file — never in this pointer.
177
+ const CRED_POINTER_PATH = pathJoin(WEBAZ_DIR, 'grant-current.json');
178
+ function saveGrantPointer(grantId, handle, capabilities, expiresAt) {
179
+ write0600(CRED_POINTER_PATH, JSON.stringify({ grant_id: grantId, handle, capabilities: capabilities ?? null, expires_at: expiresAt ?? null, stored_at: new Date().toISOString() }));
180
+ }
181
+ function readGrantPointer() {
182
+ try {
183
+ return fsExists(CRED_POINTER_PATH) ? JSON.parse(readFileSync(CRED_POINTER_PATH, 'utf8')) : null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ /**
190
+ * Resolve the stored grant bearer (reverse of storeGrantCredential). Returns null if not paired.
191
+ * ⚠️ The returned `token` is the RAW bearer — pass it only as an Authorization header; NEVER log it,
192
+ * print it, or return it in a tool response.
193
+ */
194
+ export function resolveGrantCredential() {
195
+ const ptr = readGrantPointer();
196
+ if (!ptr?.grant_id)
197
+ return null;
198
+ let token = '';
199
+ if (typeof ptr.handle === 'string' && ptr.handle.startsWith('keychain:') && process.platform === 'darwin') {
200
+ try {
201
+ token = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-a', ptr.grant_id, '-w'], { encoding: 'utf8' }).trim();
202
+ }
203
+ catch {
204
+ token = '';
205
+ }
206
+ }
207
+ if (!token) { // file fallback (also covers keychain-read failure)
208
+ try {
209
+ const store = JSON.parse(readFileSync(CRED_FALLBACK_PATH, 'utf8'));
210
+ token = store[ptr.grant_id]?.token || '';
211
+ }
212
+ catch {
213
+ token = '';
214
+ }
215
+ }
216
+ if (!token)
217
+ return null;
218
+ return { grant_id: ptr.grant_id, token, handle: ptr.handle, capabilities: ptr.capabilities, expires_at: ptr.expires_at };
219
+ }
220
+ function savePending(pairingId, codeVerifier) {
221
+ write0600(PENDING_PATH, JSON.stringify({ pairing_id: pairingId, code_verifier: codeVerifier, started_at: new Date().toISOString() }));
222
+ }
223
+ function readPending() {
224
+ try {
225
+ return fsExists(PENDING_PATH) ? JSON.parse(readFileSync(PENDING_PATH, 'utf8')) : null;
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ function clearPending() { try {
232
+ if (fsExists(PENDING_PATH))
233
+ unlinkSync(PENDING_PATH);
234
+ }
235
+ catch { /* best effort */ } }
236
+ export async function handlePair(args) {
237
+ // Mode isolation (fail-closed): every webaz_pair action (start / complete / verify) talks to the
238
+ // LIVE webaz.xyz API. In explicit sandbox mode (local-only) we must NOT touch the live network.
239
+ if (!isNetworkMode()) {
240
+ return {
241
+ _mode: MODE,
242
+ error: 'webaz_pair requires NETWORK mode — you are in sandbox (local-only, isolated from webaz.xyz). Unset WEBAZ_MODE (or set WEBAZ_MODE=network / network_readonly) to pair with / verify against a real human account.',
243
+ error_code: 'PAIRING_REQUIRES_NETWORK',
244
+ };
245
+ }
246
+ const action = args.action || 'start';
247
+ if (action === 'start') {
248
+ const caps = Array.isArray(args.capabilities) && args.capabilities.length
249
+ ? args.capabilities.map(c => ({ capability: String(c) }))
250
+ : [{ capability: 'read_public' }, { capability: 'search' }]; // default: minimal safe scopes
251
+ const verifier = generateCodeVerifier();
252
+ const challenge = pkceChallengeS256(verifier);
253
+ const resp = await apiCall('/api/agent-grants/pair/start', {
254
+ method: 'POST',
255
+ body: {
256
+ code_challenge: challenge,
257
+ capabilities: caps,
258
+ agent_label: typeof args.agent_label === 'string' ? args.agent_label : undefined,
259
+ reason: typeof args.reason === 'string' ? args.reason : undefined, // free-text reason only
260
+ },
261
+ });
262
+ if (resp.error)
263
+ return resp;
264
+ savePending(String(resp.pairing_id), verifier); // verifier stays LOCAL; never sent until retrieve
265
+ return {
266
+ status: 'awaiting_human_approval',
267
+ pairing_id: resp.pairing_id,
268
+ user_code: resp.user_code,
269
+ approve_url: `${WEBAZ_API_URL}${String(resp.approve_url || '')}`,
270
+ expires_at: resp.expires_at,
271
+ next: `Ask the human to open approve_url (logged in at webaz.xyz) and approve. Then call webaz_pair again with action="complete".`,
272
+ capabilities_requested: caps.map(c => c.capability),
273
+ note: 'Safe scopes only. The credential is delivered once on completion and stored in your OS secret store — it is never shown here.',
274
+ };
275
+ }
276
+ if (action === 'complete') {
277
+ const pending = readPending();
278
+ if (!pending)
279
+ return { error: 'no pending pairing — run webaz_pair (action="start") first', error_code: 'NO_PENDING_PAIRING' };
280
+ const resp = await apiCall(`/api/agent-grants/pair/${pending.pairing_id}/retrieve`, {
281
+ method: 'POST',
282
+ body: { code_verifier: pending.code_verifier },
283
+ });
284
+ if (resp.error) {
285
+ // not approved yet → keep pending so the human can still approve + a later retry works
286
+ if (resp.http_status === 409)
287
+ return { status: 'not_ready', detail: resp.error, hint: 'Human has not approved yet (or it expired). Approve, then call action="complete" again.' };
288
+ clearPending();
289
+ return resp;
290
+ }
291
+ const token = String(resp.token || '');
292
+ const grantId = String(resp.grant_id || '');
293
+ if (!token || !grantId) {
294
+ clearPending();
295
+ return { error: 'retrieval returned no credential', error_code: 'RETRIEVE_EMPTY' };
296
+ }
297
+ const handle = storeGrantCredential(grantId, token); // raw bearer goes straight to secret store
298
+ saveGrantPointer(grantId, handle, resp.capabilities, resp.expires_at); // non-secret index for later resolve
299
+ clearPending();
300
+ return {
301
+ status: 'stored',
302
+ credential_handle: handle, // opaque handle — NOT the token
303
+ grant_id: grantId,
304
+ capabilities: resp.capabilities,
305
+ expires_at: resp.expires_at,
306
+ note: 'Credential stored in your OS secret store (raw token NOT shown; server keeps only a hash). Use webaz_pair action="verify" to confirm it; the server enforces active/expiry/revoked/scope on every call. Safe scopes only.',
307
+ };
308
+ }
309
+ if (action === 'verify') {
310
+ // Consume the stored grant against a SAFE grant-gated route (read_public). The SERVER is authoritative:
311
+ // it re-checks active/expiry/revoked/subject-suspension/scope + audits on EVERY call. We resolve the
312
+ // bearer from the secret store and attach it; the raw token is never printed. Safe scopes only — no
313
+ // business tool and no risk scope is wired to grants here.
314
+ const cred = resolveGrantCredential();
315
+ if (!cred)
316
+ return { status: 'not_paired', error_code: 'NO_GRANT_CREDENTIAL', hint: 'No stored grant. Run webaz_pair action="start", have the human approve, then action="complete".' };
317
+ const resp = await apiCall('/api/agent-grants/whoami', { method: 'GET', apiKey: cred.token });
318
+ if (resp.error) {
319
+ return { status: 'grant_invalid', grant_id: cred.grant_id, error: resp.error, error_code: resp.error_code, hint: 'Grant is no longer valid (revoked / expired / suspended). Re-pair with webaz_pair action="start".' };
320
+ }
321
+ return {
322
+ status: 'active',
323
+ grant: resp.grant, // SERVER-AUTHORITATIVE principal (grant_id/human_id/agent_label/capability)
324
+ local_cache: { capabilities: cred.capabilities, expires_at: cred.expires_at }, // advisory only, may be stale — NOT authoritative
325
+ note: 'Grant verified LIVE by the server (per-call: active/expiry/revoked/subject-suspension/scope + audited). `grant` is authoritative; `local_cache` is advisory. Safe scopes only; no business tool or risk scope consumes this grant.',
326
+ };
327
+ }
328
+ return { error: `unknown action "${action}" — use "start" | "complete" | "verify"`, error_code: 'BAD_ACTION' };
329
+ }
183
330
  // 启动 banner(stderr)+ status 声明用 —— 让用户/agent 一眼知道现在是真网络还是沙盒
184
331
  function modeBanner() {
185
332
  if (MODE === 'network') {
@@ -195,6 +342,13 @@ function modeBanner() {
195
342
  // ─── 初始化 ──────────────────────────────────────────────────
196
343
  const db = initDatabase();
197
344
  setSeamDb(db); // RFC-016 Phase 1:注入异步 DB seam(本进程)—— 共享引擎迁 seam 后 MCP 进程也能用,否则 dbOne/dbAll 抛"未初始化"
345
+ // MCP fresh-DB schema bridge: apply the SAME pure schema helpers the PWA boot
346
+ // runs (incl. product_aliases + users.permanent_code/handle/region), so an
347
+ // MCP-initialized DB is schema-complete for the sandbox tool path WITHOUT first
348
+ // booting the PWA server. Pure idempotent DDL only — no business writes, no
349
+ // money/order/status path. (MCP_PRODUCT_COLS below now overlaps the products
350
+ // columns and is redundant-but-harmless; left untouched to avoid scope creep.)
351
+ applyWebazRuntimeSchema(db);
198
352
  initSystemUser(db);
199
353
  initDisputeSchema(db);
200
354
  initNotificationSchema(db);
@@ -325,6 +479,26 @@ No auth required, no parameters needed.
325
479
  properties: {},
326
480
  },
327
481
  },
482
+ {
483
+ name: 'webaz_pair',
484
+ description: `Pair this agent with a human's WebAZ account via a Passkey-approved, scoped, short-lived **delegation grant** (RFC-020) — NOT by pasting a permanent api_key. Two steps:
485
+ 1. action="start" → returns an approve_url + short user_code. The human opens it at webaz.xyz (logged in) and approves the server-shown consent (safe scopes only).
486
+ 2. action="complete" → retrieves the credential ONCE (PKCE) and stores it in your OS secret store (macOS Keychain → ~/.webaz/credentials 0600 fallback). Returns only a credential_handle + status — the raw token is never shown.
487
+ 3. action="verify" → uses the stored grant against a safe read endpoint to confirm it is still valid. The server re-checks active/expiry/revoked/suspension/scope and audits the call every time. Returns the grant principal/status; never the raw token.
488
+
489
+ Scopes: SAFE only (read_public, profile_read, search, list_product_draft, product_publish_request, draft_order). Risk scopes (place_order/wallet/refund/...) and never-delegable actions (withdraw/key-change/vote/...) are hard-rejected.
490
+
491
+ NOTE: this consumes the grant only on safe read paths; no business tool and no risk scope is wired to grants. No account is created (registration stays human-only at webaz.xyz).`,
492
+ inputSchema: {
493
+ type: 'object',
494
+ properties: {
495
+ action: { type: 'string', enum: ['start', 'complete', 'verify'], description: 'start a pairing, complete it after the human approves, or verify the stored grant (default: start)' },
496
+ capabilities: { type: 'array', items: { type: 'string' }, description: 'Requested SAFE scopes (default: read_public, search). Non-safe scopes are rejected.' },
497
+ agent_label: { type: 'string', description: 'Human-friendly name for this agent (shown in the consent screen)' },
498
+ reason: { type: 'string', description: 'Free-text reason shown to the human (you cannot relabel the scopes)' },
499
+ },
500
+ },
501
+ },
328
502
  {
329
503
  name: 'webaz_register',
330
504
  // was ~1732 chars, now ~780 chars
@@ -1722,10 +1896,10 @@ async function handleInfo() {
1722
1896
  ? '🟢 NETWORK(只读):无 api_key。公共读(搜索/榜单/价格史/浏览)打 webaz.xyz 真网络(见 network_live)。要交易(注册/下单/上架等)请设 WEBAZ_API_KEY —— 到 ' + WEBAZ_API_URL + '/#welcome 申请邀请。'
1723
1897
  : '🟡 SANDBOX:所有工具都在本机本地 SQLite 运行,与 webaz.xyz 全网隔离(显式 dev/demo 模式)。任何计数 / 账号 / 订单仅本机有效。不设 WEBAZ_MODE 则默认走真网络只读。',
1724
1898
  phase: 'pre_launch',
1725
- real_users_on_canonical: 0,
1899
+ // 真实用户/规模【不在此硬编码】—— 网络模式下 network_live(上方,实时拉 /api/protocol-status)带有 real_users_on_canonical 真值。
1726
1900
  canonical_endpoint: 'https://webaz.xyz',
1727
1901
  economic_flow: 'simulated WAZ (test currency, 1 WAZ ≈ 1 USDC peg is a模拟基准, not a real exchange rate). No fiat/crypto settles yet.',
1728
- disclaimer: '本协议尚未公开上线,prod 真实用户≈0,无真实经济流转。下方 live_stats / 工具示例所有计数均来自**本机 MCP 服务器的本地 SQLite**(~/.webaz/webaz.db),仅供 dev / demo,不代表协议全网真实状态。请勿据此评估市场规模、做投资决策、或替终端用户承诺任何经济关系。',
1902
+ disclaimer: '本协议尚未公开上线(pre-launch),无真实经济流转。真实用户/规模请以 network_live(本次从 webaz.xyz 实时拉取的 /api/protocol-status,含 real_users_on_canonical)为准;下方 live_stats / 工具示例的所有计数均来自**本机 MCP 服务器的本地 SQLite**(~/.webaz/webaz.db),仅供 dev / demo,不代表协议全网真实状态。请勿据此评估市场规模、做投资决策、或替终端用户承诺任何经济关系。',
1729
1903
  },
1730
1904
  description: 'WebAZ is a decentralized commerce protocol. Every transaction flows through a state machine; each state transition needs proof-of-action from the responsible party. Any timeout-without-action: protocol auto-rules default + executes remedy. / WebAZ 是去中心化商业协议;每笔交易通过状态机流转,每次状态转移需对应责任方的操作证明;超时未操作则协议自动判违约并执行处置。',
1731
1905
  // 给【终端用户/买家】的价值主张 —— 陌生 agent / 普通买家先要知道"对我有什么用",而非只读抽象 tagline + 技术 description。
@@ -1819,7 +1993,7 @@ function handleRegister(args) {
1819
1993
  ],
1820
1994
  register_url: WEBAZ_API_URL,
1821
1995
  why_not_agent_self_register: 'agent 自助注册会绕过邀请 / captcha / 真人 Passkey 责任制,破坏协议的可问责性(#6 不滥用 / CHARTER §4 I-5)。',
1822
- want_to_try_offline_first: '只想离线试玩、暂不连网络?设环境变量 WEBAZ_MODE=sandbox(或清空 WEBAZ_API_KEY),webaz_register 会在本机沙盒建一个测试账号(仅本机有效)。',
1996
+ want_to_try_offline_first: '只想离线试玩、暂不连网络?设环境变量 WEBAZ_MODE=sandbox(本机沙盒需显式开启;无 key 仍是 NETWORK 只读,不会进沙盒),webaz_register 会在本机沙盒建一个测试账号(仅本机有效)。',
1823
1997
  };
1824
1998
  }
1825
1999
  // ─── SANDBOX 模式:本机建测试账号(显著标注,仅本机有效,与 webaz.xyz 全网隔离)──
@@ -2347,8 +2521,8 @@ async function handleListProduct(args) {
2347
2521
  id, seller_id, title, description, price, stock, category, stake_amount,
2348
2522
  specs, brand, model, source_price,
2349
2523
  ship_regions, handling_hours, estimated_days, fragile,
2350
- return_days, return_condition, warranty_days
2351
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2524
+ return_days, return_condition, warranty_days, currency
2525
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'WAZ')
2352
2526
  `).run(id, user.id, args.title, args.description, price, args.stock ?? 1, args.category ?? null, stakeAmount, specsJson, args.brand ?? null, args.model ?? null, args.source_price != null ? Number(args.source_price) : null, args.ship_regions ?? '全国', args.handling_hours != null ? Number(args.handling_hours) : 24, estJson, args.fragile ? 1 : 0, args.return_days != null ? Number(args.return_days) : 7, args.return_condition ?? '', args.warranty_days != null ? Number(args.warranty_days) : 0);
2353
2527
  // 注:per-order stake 模型不在此 lock — 移到 place_order 时按订单总额锁
2354
2528
  // S2/S3/S4 新字段:通过 HTTP PUT 走标准校验路径(i18n 语言白名单 / origin_claims 4KB+sha256 / 库存阈值)
@@ -3536,13 +3710,18 @@ async function handleReferral(args) {
3536
3710
  state = 'never_activated';
3537
3711
  note = 'Rewards inactive — attributions recorded; commissions held in pending_commission_escrow (30d window per protocol_params.rewards_opt_in.escrow_days) until you activate via PWA #me.';
3538
3712
  }
3539
- const pending = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending'").get(userId);
3540
- const expired = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired'").get(userId);
3713
+ // RFC-018: matures_at IS NULL = opt-out escrow only (clearing rows auto-mature; surfaced separately in PR3).
3714
+ const pending = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NULL").get(userId);
3715
+ const expired = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired' AND matures_at IS NULL").get(userId);
3716
+ // RFC-018 clearing window: opted-in commission accrued but not yet matured into the wallet
3717
+ // (matures_at NOT NULL). Distinct from opt-out escrow above — pure read, surfaced for the agent.
3718
+ const clearing = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending' AND matures_at IS NOT NULL").get(userId);
3541
3719
  return {
3542
3720
  state,
3543
3721
  opted_in: optIn === 1,
3544
3722
  note,
3545
3723
  pending_escrow: { count: pending.n, total_amount: pending.total },
3724
+ clearing: { count: clearing.n, total_amount: clearing.total }, // RFC-018: accrued, maturing after the return window (not yet paid)
3546
3725
  expired_to_charity: { count: expired.n, total_amount: expired.total },
3547
3726
  spec: 'RFC-002 §3.5 — rewards / share-commission opt-in (RFC-002)',
3548
3727
  };
@@ -4948,7 +5127,7 @@ export async function startMCPServer() {
4948
5127
  // ─── RFC-003 Batch 0 安全网:NETWORK 模式下未迁移的工具【硬失败】,不静默落本地沙盒 ───
4949
5128
  // 例外:info / register(NETWORK_SELF_AWARE)有专门 network-aware 处理,照常放行。
4950
5129
  let handled = false;
4951
- if (isNetworkMode() && !NETWORK_TOOLS.has(name) && !NETWORK_SELF_AWARE.has(name)) {
5130
+ if (isNetworkMode() && !toolAllowedInNetworkMode(name)) {
4952
5131
  result = networkMigrationPending(name);
4953
5132
  handled = true;
4954
5133
  }
@@ -4957,6 +5136,9 @@ export async function startMCPServer() {
4957
5136
  case 'webaz_info':
4958
5137
  result = await handleInfo();
4959
5138
  break;
5139
+ case 'webaz_pair':
5140
+ result = await handlePair(args);
5141
+ break;
4960
5142
  case 'webaz_register':
4961
5143
  result = handleRegister(args);
4962
5144
  break;
@@ -5,7 +5,7 @@
5
5
  * 核心思路:解决冷启动——现有 Amazon/Shopify 卖家零成本接入新渠道。
6
6
  *
7
7
  * Skill 类型:
8
- * catalog_sync - 目录同步(把外部店铺接入 DCP 搜索)
8
+ * catalog_sync - 目录同步(把外部店铺接入 WebAZ 搜索)
9
9
  * auto_accept - 自动接单(买家下单后立即接受,无需等待)
10
10
  * price_negotiation - 价格协商(允许 Agent 在限定范围内议价)
11
11
  * quality_guarantee - 质量承诺(额外质押,增强买家信心)
@@ -67,8 +67,8 @@ export const SKILL_TYPE_META = {
67
67
  label: '目录同步',
68
68
  label_en: 'Catalog sync',
69
69
  icon: '🔄',
70
- description: '将卖家的外部商品目录(Amazon/Shopify/自定义)同步到 DCP,买家 Agent 订阅后可优先发现这些商品。',
71
- description_en: 'Sync seller external catalogs (Amazon/Shopify/custom) to DCP — subscribed buyer agents discover these first.',
70
+ description: '将卖家的外部商品目录(Amazon/Shopify/自定义)同步到 WebAZ,买家 Agent 订阅后可优先发现这些商品。',
71
+ description_en: 'Sync seller external catalogs (Amazon/Shopify/custom) to WebAZ — subscribed buyer agents discover these first.',
72
72
  },
73
73
  auto_accept: {
74
74
  label: '自动接单',
@@ -95,7 +95,7 @@ export const SKILL_TYPE_META = {
95
95
  label: '质量承诺',
96
96
  label_en: 'Quality guarantee',
97
97
  icon: '🛡️',
98
- description: '卖家额外质押 DCP 作为质量保证金,问题订单买家可获得额外赔偿。',
98
+ description: '卖家额外质押 WAZ 作为质量保证金,问题订单买家可获得额外赔偿。',
99
99
  description_en: 'Seller stakes extra WAZ as quality bond — buyer gets additional compensation on disputes.',
100
100
  },
101
101
  instant_ship: {
package/dist/ledger.js CHANGED
@@ -1,9 +1,18 @@
1
1
  import { toUnits, toDecimal } from './money.js';
2
2
  /** 读某用户钱包当前余额(整数 base-units)。无钱包行 → 全 0。 */
3
3
  export function walletUnits(db, userId) {
4
- const r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
5
- .get(userId);
6
- return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0) };
4
+ // fee_staked Direct Pay 新增列。向后兼容缺该列的库(迁移前 / 测试用最小 schema):
5
+ // 先按含 fee_staked 查;若该列不存在(no such column)→ 退回 4 列查,fee_staked=0。
6
+ let r;
7
+ try {
8
+ r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned, COALESCE(fee_staked,0) fee_staked FROM wallets WHERE user_id = ?')
9
+ .get(userId);
10
+ }
11
+ catch {
12
+ r = db.prepare('SELECT COALESCE(balance,0) balance, COALESCE(staked,0) staked, COALESCE(escrowed,0) escrowed, COALESCE(earned,0) earned FROM wallets WHERE user_id = ?')
13
+ .get(userId);
14
+ }
15
+ return { balance: toUnits(r?.balance ?? 0), staked: toUnits(r?.staked ?? 0), escrowed: toUnits(r?.escrowed ?? 0), earned: toUnits(r?.earned ?? 0), fee_staked: toUnits(r?.fee_staked ?? 0) };
7
16
  }
8
17
  /**
9
18
  * 对钱包字段施加整数单位 delta,以【绝对值】落库(读当前→加 delta→写 toDecimal)。
package/dist/mcp.js CHANGED
@@ -4,12 +4,31 @@
4
4
  *
5
5
  * 运行:npm run mcp(或 npx @seasonkoh/webaz);通过 stdio 与 Claude 等 MCP 客户端通信。
6
6
  * 工具实现都在 src/layer1-agent/L1-1-mcp-server/server.ts(本文件只是 bootstrap)。
7
- * 双模:配 WEBAZ_API_KEY → NETWORK( webaz.xyz 共享网络);否则 SANDBOX(本机库)。详见 RFC-003。
7
+ * 模式:配 WEBAZ_API_KEY → NETWORK(可交易,调 webaz.xyz 共享网络);无 key → NETWORK 只读(公共读走 webaz.xyz);
8
+ * 仅 WEBAZ_MODE=sandbox 才用本机库(与全网隔离,dev/demo)。默认【不是】sandbox。详见 RFC-003。
8
9
  *
9
- * 关联 / Related: AGENTS.md(项目地图) · RFC-003(双模) · RFC-004(webaz_feedback)
10
+ * 关联 / Related: AGENTS.md(项目地图) · RFC-003(三态:network / network_readonly / sandbox) · RFC-004(webaz_feedback)
10
11
  */
11
- import { startMCPServer } from './layer1-agent/L1-1-mcp-server/server.js';
12
- startMCPServer().catch((err) => {
12
+ // ⚠️ 只【静态】import cli.js(纯:version + network-mode,无副作用)。【不】静态 import server.js ——
13
+ // server.js 顶层会 initDatabase()/建 schema,静态 import 会让 --version/--help/--mode 也触发 DB 副作用
14
+ // (写 ~/.webaz/*.db)+ 加载整个 server(#186 审计 P1)。故 server.js 只在真正启动时【动态】import。
15
+ import { cliQuickResponse, runDoctor } from './layer1-agent/L1-1-mcp-server/cli.js';
16
+ async function main() {
17
+ const argv = process.argv.slice(2);
18
+ const quick = cliQuickResponse(argv, process.env); // --version / --help / --mode(纯,不碰 DB/server)
19
+ if (quick !== null) {
20
+ console.log(quick);
21
+ process.exit(0);
22
+ }
23
+ if (argv.includes('--doctor')) {
24
+ console.log(await runDoctor(process.env));
25
+ process.exit(0);
26
+ }
27
+ // 只有真正启动 stdio server 时才动态加载 server.js(此时才允许 DB 初始化等副作用)。
28
+ const { startMCPServer } = await import('./layer1-agent/L1-1-mcp-server/server.js');
29
+ await startMCPServer();
30
+ }
31
+ main().catch((err) => {
13
32
  console.error('MCP Server 启动失败:', err);
14
33
  process.exit(1);
15
34
  });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Merchant Base-Bond (v1 collateral-only) — domain skeleton (PR1, testnet/dev scaffold).
3
+ *
4
+ * 设计真相源:docs/modules/MERCHANT-BASE-BOND-DESIGN.INTERNAL.md(v1 candidate-locked)。
5
+ * 本模块是【纯域逻辑骨架】:状态机 + 资格派生 + 类型。**不写 DB、不接任何 live 路径、不碰资金。**
6
+ *
7
+ * v1 已锁(见设计稿 §12):
8
+ * - 链上非托管锁仓合约持有 collateral;买家货款永不进合约;平台费【不上链】(链下账务)。
9
+ * - slash 仅治理触发 + 链上硬约束(v1 无自动/小额 slash)。
10
+ * - 合约 v1 不可升级;链 = Base + 白名单 USDC(单链);入门门 = 单一 base_bond_min_units(不分档)。
11
+ * - wallet rotation = seller 签名 + WebAZ 授权;治理仅紧急。
12
+ * - 计价用 base_bond_min_units(USDC 整数单位),不引预言机。
13
+ *
14
+ * ⚠️ MERCHANT_BOND_V1_ENABLED 默认 false:整套 v1 在 testnet/dev scaffold 阶段【关闭】,
15
+ * mainnet + 真实 USDC 需先过 法务 + 外部合约审计 + Holden 批准(设计稿 §9)。
16
+ */
17
+ /** 全局开关:v1 默认关闭(PR1 scaffold)。真实启用前永远 false。 */
18
+ export const MERCHANT_BOND_V1_ENABLED = false;
19
+ /** v1 链:Base + 白名单 USDC(单链;仅 chain profile 占位,无多链抽象)。 */
20
+ export const MERCHANT_BOND_CHAIN_PROFILE = {
21
+ key: 'base',
22
+ // 真实 chainId / USDC 地址 / 确认数 N 在 mainnet 接入时由治理/配置定稿(政策参数);此处仅占位。
23
+ chainId: null,
24
+ usdcAddress: null,
25
+ minConfirmations: null,
26
+ };
27
+ /** Seller 生命周期状态(枚举;`paused` 不是 seller 状态,pause 是正交 flags)。 */
28
+ export const BOND_STATUS = {
29
+ NONE: 'none',
30
+ PENDING_CONFIRMATIONS: 'pending_confirmations',
31
+ ACTIVE: 'active',
32
+ COOLING: 'cooling',
33
+ WITHDRAWABLE: 'withdrawable',
34
+ WITHDRAWN: 'withdrawn',
35
+ SLASHED_BELOW_MIN: 'slashed_below_min',
36
+ };
37
+ /** 合法状态转移(域层薄校验;真相仍以链上合约 event 为准)。 */
38
+ const VALID_TRANSITIONS = {
39
+ [BOND_STATUS.NONE]: [BOND_STATUS.PENDING_CONFIRMATIONS],
40
+ [BOND_STATUS.PENDING_CONFIRMATIONS]: [BOND_STATUS.ACTIVE, BOND_STATUS.NONE],
41
+ [BOND_STATUS.ACTIVE]: [BOND_STATUS.COOLING, BOND_STATUS.SLASHED_BELOW_MIN],
42
+ [BOND_STATUS.COOLING]: [BOND_STATUS.WITHDRAWABLE, BOND_STATUS.ACTIVE],
43
+ [BOND_STATUS.WITHDRAWABLE]: [BOND_STATUS.WITHDRAWN, BOND_STATUS.ACTIVE],
44
+ [BOND_STATUS.WITHDRAWN]: [],
45
+ [BOND_STATUS.SLASHED_BELOW_MIN]: [BOND_STATUS.ACTIVE],
46
+ };
47
+ export function canTransitionBond(from, to) {
48
+ return (VALID_TRANSITIONS[from] || []).includes(to);
49
+ }
50
+ /**
51
+ * 资格派生(单一真相函数:readiness 与 create gate 必须都调它,定义逐字一致)。
52
+ * fail-closed:开关关 / 未绑钱包 / 未确认 / 状态非 active / 担保不足 → false。
53
+ * v1 入门门 = 单一 base_bond_min_units(不分档)。
54
+ */
55
+ export function deriveSellerHasProductionBaseBondLocked(view, baseBondMinUnits) {
56
+ if (!MERCHANT_BOND_V1_ENABLED)
57
+ return false; // v1 scaffold 默认关 → 永远不翻真实资格
58
+ if (!view)
59
+ return false;
60
+ return view.walletBound
61
+ && view.confirmed
62
+ && view.status === BOND_STATUS.ACTIVE
63
+ && view.collateralUnits >= baseBondMinUnits;
64
+ }