@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
@@ -0,0 +1,78 @@
1
+ import { toUnits } from './money.js';
2
+ export const EXPOSURE_FACTOR_BPS_PARAM = 'direct_pay.exposure_factor_bps';
3
+ export const EXPOSURE_FACTOR_BPS_DEFAULT = 8000;
4
+ export class ExposureCapConfigError extends Error {
5
+ }
6
+ /** fail-closed 读取 factor:整数 ∈ [1,10000];缺失/非数/越界 → 抛 ExposureCapConfigError。 */
7
+ export function readExposureFactorBps(getProtocolParam) {
8
+ const raw = getProtocolParam(EXPOSURE_FACTOR_BPS_PARAM, undefined);
9
+ const n = Number(raw);
10
+ if (raw === undefined || raw === null || raw === '' || !Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 10000) {
11
+ throw new ExposureCapConfigError(`${EXPOSURE_FACTOR_BPS_PARAM} invalid (got ${JSON.stringify(raw)}); must be integer in [1,10000]`);
12
+ }
13
+ return n;
14
+ }
15
+ /** 卖家当前有效抵押(USDC integer units,big-int 安全):merchant_bond_deposits status='active' 之和。无 → 0n。 */
16
+ export function getActiveCollateralUnits(db, sellerId) {
17
+ let rows = [];
18
+ // 表缺失(旧/最小库)→ 视作无抵押(0n),休眠安全:不让缺表破坏现有直付建单。
19
+ try {
20
+ rows = db.prepare("SELECT collateral_units FROM merchant_bond_deposits WHERE seller_id = ? AND status = 'active'").all(sellerId);
21
+ }
22
+ catch {
23
+ return 0n;
24
+ }
25
+ let sum = 0n;
26
+ for (const r of rows) {
27
+ try {
28
+ sum += BigInt(r.collateral_units || '0');
29
+ }
30
+ catch { /* 坏行跳过,不计入(fail-closed:少算抵押更保守) */ }
31
+ }
32
+ return sum;
33
+ }
34
+ /**
35
+ * 开放敞口状态(未完全关闭的 direct_p2p 单)。终态【不】计入(释放敞口):
36
+ * completed / cancelled / fault_* / resolved_for_seller / refunded_partial / refunded_full / dispute_dismissed / expired。
37
+ * ⚠️ direct_expired_unconfirmed 是【非终态】(超时不静默关单,仍可转 disputed / cancelled,见 transitions.ts)→ 必须计入,
38
+ * 漏算会低估敞口(对风险闸是危险方向)。created 为建单原子前态(direct_p2p 一般即转 window),防御性纳入(过度计入只会更严、无害)。
39
+ */
40
+ export const OPEN_EXPOSURE_STATUSES = [
41
+ 'created', 'direct_pay_window', 'direct_expired_unconfirmed',
42
+ 'accepted', 'shipped', 'picked_up', 'in_transit', 'delivered', 'confirmed', 'disputed',
43
+ ];
44
+ /** 卖家当前 Direct Pay 开放敞口(units):未完全关闭的 direct_p2p 单 total_amount 之和。 */
45
+ export function computeDirectPayOpenExposureUnits(db, sellerId) {
46
+ const placeholders = OPEN_EXPOSURE_STATUSES.map(() => '?').join(',');
47
+ const rows = db.prepare(`SELECT total_amount FROM orders WHERE seller_id = ? AND payment_rail = 'direct_p2p' AND status IN (${placeholders})`).all(sellerId, ...OPEN_EXPOSURE_STATUSES);
48
+ let sum = 0n;
49
+ for (const r of rows)
50
+ sum += BigInt(toUnits(Number(r.total_amount) || 0));
51
+ return sum;
52
+ }
53
+ /** 纯函数:本单是否在敞口上限内。allowed = collateral × bps / 10000;open + new ≤ allowed。 */
54
+ export function withinExposureCap(args) {
55
+ const allowed = args.activeCollateralUnits * BigInt(args.factorBps) / 10000n;
56
+ return args.openExposureUnits + args.newOrderUnits <= allowed;
57
+ }
58
+ /**
59
+ * create-gate 主入口。休眠安全:collateral==0 → ok(N/A,缓交路径由 deferral quota 管,不读 factor)。
60
+ * collateral>0 → 读 factor(fail-closed)+ 比较;超出 → 拒(EXPOSURE_CAP_EXCEEDED)。
61
+ */
62
+ export function enforceCollateralExposureGate(db, sellerId, newOrderUnits, getProtocolParam) {
63
+ const collateral = getActiveCollateralUnits(db, sellerId);
64
+ if (collateral <= 0n)
65
+ return { ok: true }; // 无真实抵押 → 敞口上限不适用(休眠)
66
+ let factorBps;
67
+ try {
68
+ factorBps = readExposureFactorBps(getProtocolParam);
69
+ }
70
+ catch (e) {
71
+ return { ok: false, error_code: 'EXPOSURE_CAP_CONFIG', reason: e.message };
72
+ } // fail-closed
73
+ const openExposureUnits = computeDirectPayOpenExposureUnits(db, sellerId);
74
+ if (!withinExposureCap({ activeCollateralUnits: collateral, openExposureUnits, newOrderUnits: BigInt(newOrderUnits), factorBps })) {
75
+ return { ok: false, error_code: 'EXPOSURE_CAP_EXCEEDED', reason: 'direct-pay open exposure exceeds collateral-backed cap' };
76
+ }
77
+ return { ok: true };
78
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Merchant Base-Bond — chain watcher skeleton (PR1, testnet/dev scaffold).
3
+ *
4
+ * 设计:docs/modules/MERCHANT-BASE-BOND-DESIGN.INTERNAL.md §4.1/§4.2。
5
+ * 职责(未来 testnet 才接真实 RPC):监听合约 deposit/slash/withdraw event → ≥N confirmations 后
6
+ * 更新 DB 镜像(merchant_bond_*)。reorg 后冻结派生资格 + 告警。
7
+ *
8
+ * ⚠️ PR1 阶段:**纯骨架、默认关、不连任何 RPC、不写资金、不接 live 路径。** 启用前需:
9
+ * chain profile 定稿 + 法务 + 外部合约审计 + Holden 批准(设计稿 §9)。
10
+ */
11
+ import { MERCHANT_BOND_V1_ENABLED, MERCHANT_BOND_CHAIN_PROFILE } from './merchant-bond-domain.js';
12
+ /**
13
+ * 启动 watcher。**PR1:若未启用(默认)或 chain profile 未定稿 → 直接返回 no-op,绝不连 RPC。**
14
+ * 真实事件处理(deposit recognition / confirmations / reorg freeze)留待后续 testnet PR。
15
+ */
16
+ export function startMerchantBondWatcher(_deps = {}) {
17
+ const profileReady = MERCHANT_BOND_CHAIN_PROFILE.chainId != null
18
+ && MERCHANT_BOND_CHAIN_PROFILE.usdcAddress != null
19
+ && MERCHANT_BOND_CHAIN_PROFILE.minConfirmations != null;
20
+ if (!MERCHANT_BOND_V1_ENABLED || !profileReady) {
21
+ // disabled scaffold:no RPC、no timer、no DB write。
22
+ return { enabled: false, stop: () => { } };
23
+ }
24
+ // 真实监听实现 = 后续 testnet PR(deposit/slash/withdraw event → ≥N confirmations → DB 镜像;reorg 冻结+告警)。
25
+ throw new Error('MerchantBondWatcher: real chain listening not implemented in PR1 scaffold');
26
+ }
@@ -0,0 +1,29 @@
1
+ import { takeFeeAtCompletion, releaseFeeStake, slashFeeStakeToPenalty } from './direct-pay-ledger.js';
2
+ /**
3
+ * Rail 1 = 非托管场外直付 + 信誉(buyer protection = reputation-only, refund = none)。
4
+ * ⚠️ 本 seam 当前【未接线】(getPaymentRail 无调用方)。Rail1 平台费【实时路径】= 首单宽限 + 预充值续用:
5
+ * 建单走【首单宽限 + 预充值门】(direct-pay-create.ts),完成时 accrue 应收(server.ts settleOrder direct_p2p 分支)。
6
+ * 下方 collectFeeAtCompletion/release/slash 仍指向旧 WAZ fee-stake helper = 遗留/forward-compat 占位;
7
+ * 若将来真接线本 seam,须改为应收/预充值语义(accrue / prepay)。
8
+ */
9
+ export const RAIL_DIRECT_P2P = {
10
+ contract: {
11
+ id: 'direct_p2p',
12
+ buyerProtection: 'reputation_only',
13
+ refund: 'none',
14
+ disputeRemedyBasis: 'evidence_reputation',
15
+ custodialOfTradeFunds: false,
16
+ },
17
+ collectFeeAtCompletion: (db, orderId) => takeFeeAtCompletion(db, { orderId }),
18
+ releaseOnExpiryOrCancel: (db, orderId) => releaseFeeStake(db, { orderId }),
19
+ slashOnFault: (db, orderId, txnId, reason) => slashFeeStakeToPenalty(db, { orderId, txnId, reason }),
20
+ };
21
+ /**
22
+ * 取轨道实现。只 Rail 1 已实现;'escrow' 走既有系统(不经此抽象);'onchain_full_stake' / 'psp' = GATED(未实现)。
23
+ * 故意用显式 switch,不建插件注册框架(anti-YAGNI)。
24
+ */
25
+ export function getPaymentRail(railId) {
26
+ if (railId === 'direct_p2p')
27
+ return RAIL_DIRECT_P2P;
28
+ return null; // escrow=既有系统; onchain_full_stake/psp=未实现(forward-compat 契约见上)
29
+ }
@@ -0,0 +1,93 @@
1
+ export const MAX_URL_LEN = 2048;
2
+ export const MAX_PLATFORM_LEN = 60;
3
+ export const MAX_NOTES_LEN = 500;
4
+ const ACTIVE = "('issued','submitted','verified')";
5
+ const getActiveForProduct = (db, productId) => db.prepare(`SELECT * FROM product_verifications WHERE product_id = ? AND status IN ${ACTIVE} ORDER BY created_at DESC, rowid DESC LIMIT 1`).get(productId);
6
+ /** http(s) 链接最弱校验(仅存储,不抓取):必须 http/https、长度受限。拒 javascript:/data: 等危险 scheme。 */
7
+ function isStorableHttpUrl(url) {
8
+ if (typeof url !== 'string' || url.length === 0 || url.length > MAX_URL_LEN)
9
+ return false;
10
+ return /^https?:\/\/[^\s]+$/i.test(url.trim());
11
+ }
12
+ /** 卖家为【某产品】申请认证 → issued(签发 code)。绝不自动 verify。单一活跃(per product)。调用方须先校验 seller 拥有该产品。 */
13
+ export function requestProductVerification(db, args) {
14
+ const { id, productId, sellerId, code } = args;
15
+ if (!id || !productId || !sellerId || !code)
16
+ return { ok: false, reason: 'missing id/productId/sellerId/code' };
17
+ const platform = args.platform ? String(args.platform).trim().slice(0, MAX_PLATFORM_LEN) : null;
18
+ const active = getActiveForProduct(db, productId);
19
+ if (active)
20
+ return { ok: false, reason: `该产品已有进行中的认证(${active.status})` };
21
+ db.prepare(`INSERT INTO product_verifications (id, product_id, seller_id, code, platform, status, created_at, updated_at)
22
+ VALUES (?,?,?,?,?, 'issued', datetime('now'), datetime('now'))`).run(id, productId, sellerId, code, platform);
23
+ return { ok: true, status: 'issued', code };
24
+ }
25
+ /** 卖家为【某产品】提交外部链接 → submitted。要求该产品有 issued 记录;链接仅存储(不抓取)。调用方须校验 seller 拥有该产品。 */
26
+ export function submitProductVerificationLink(db, args) {
27
+ const url = String(args.externalUrl || '').trim();
28
+ if (!isStorableHttpUrl(url))
29
+ return { ok: false, reason: '请提交有效的 http(s) 商品链接' };
30
+ const active = getActiveForProduct(db, args.productId);
31
+ if (!active)
32
+ return { ok: false, reason: '请先为该产品申请认证获取验证码' };
33
+ if (active.status === 'verified')
34
+ return { ok: false, reason: '该产品认证已通过,无需重复提交' };
35
+ const platform = args.platform != null ? String(args.platform).trim().slice(0, MAX_PLATFORM_LEN) : active.platform;
36
+ db.prepare(`UPDATE product_verifications SET external_url = ?, platform = ?, status = 'submitted', updated_at = datetime('now') WHERE id = ?`)
37
+ .run(url, platform, active.id);
38
+ return { ok: true, status: 'submitted' };
39
+ }
40
+ /** 真人 admin 手动核对结论 → verified | rejected。仅从 submitted 流转。reviewerId 必填(责任分层;ROOT/Passkey 由调用方强制)。 */
41
+ export function reviewProductVerification(db, args) {
42
+ const { id, reviewerId, decision } = args;
43
+ if (!reviewerId)
44
+ return { ok: false, reason: 'reviewProductVerification requires a human reviewerId' };
45
+ if (decision !== 'verified' && decision !== 'rejected')
46
+ return { ok: false, reason: 'decision must be verified|rejected' };
47
+ const row = db.prepare('SELECT * FROM product_verifications WHERE id = ?').get(id);
48
+ if (!row)
49
+ return { ok: false, reason: 'product verification not found' };
50
+ if (row.status === decision)
51
+ return { ok: true, status: decision, already: true };
52
+ if (row.status !== 'submitted')
53
+ return { ok: false, reason: `cannot review from status '${row.status}' (need submitted)` };
54
+ const notes = args.notes != null ? String(args.notes).slice(0, MAX_NOTES_LEN) : null;
55
+ db.prepare(`UPDATE product_verifications SET status = ?, reviewed_by = ?, reviewed_at = datetime('now'), notes = ?, updated_at = datetime('now') WHERE id = ?`)
56
+ .run(decision, reviewerId, notes, id);
57
+ return { ok: true, status: decision };
58
+ }
59
+ /** 某产品最新一条认证记录(任意状态)。无 → null。 */
60
+ export function getProductVerification(db, productId) {
61
+ return db.prepare('SELECT * FROM product_verifications WHERE product_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1').get(productId) ?? null;
62
+ }
63
+ /** 某卖家所有产品认证记录(供卖家自助面板逐产品展示状态)。最新在前。 */
64
+ export function listSellerProductVerifications(db, sellerId) {
65
+ return db.prepare('SELECT * FROM product_verifications WHERE seller_id = ? ORDER BY created_at DESC, rowid DESC').all(sellerId);
66
+ }
67
+ export function toSellerProductVerificationView(row) {
68
+ return {
69
+ id: row.id, product_id: row.product_id, code: row.code, platform: row.platform, external_url: row.external_url,
70
+ status: row.status, reviewed_at: row.reviewed_at, created_at: row.created_at, updated_at: row.updated_at,
71
+ }; // 故意省略 reviewed_by + notes(admin 身份 / 内部备注)
72
+ }
73
+ /** admin 队列:按 status 过滤(默认全部),最新在前。纯读。 */
74
+ export function listProductVerifications(db, opts = {}) {
75
+ if (opts.status)
76
+ return db.prepare(`SELECT * FROM product_verifications WHERE status = ? ORDER BY created_at DESC, rowid DESC`).all(opts.status);
77
+ return db.prepare(`SELECT * FROM product_verifications ORDER BY created_at DESC, rowid DESC`).all();
78
+ }
79
+ /** 硬门读取:该产品是否【已验证】(有 verified 记录)。供 direct-pay create/availability 强制(未验证 → 不可直付)。 */
80
+ export function productStoreVerified(db, productId) {
81
+ return !!db.prepare("SELECT 1 FROM product_verifications WHERE product_id = ? AND status = 'verified' LIMIT 1").get(productId);
82
+ }
83
+ /**
84
+ * 反作弊生命周期:商品发生【重大编辑】(标题/价格/详情/外部链接)时作废其验证 → status='stale'。
85
+ * 之后 productStoreVerified=false(硬门重新拦截),且 active 记录被清(卖家可重新申领→提交→admin 重核)。
86
+ * 防"先把商品 A 验证通过,再改标题/价格/详情/链接变成商品 B"绕过逐品门。返回作废条数。纯写,只改本表。
87
+ * 注:store-level 豁免卖家不依赖逐品验证,本作废对其直付资格无影响。
88
+ */
89
+ export function invalidateProductVerification(db, productId) {
90
+ const r = db.prepare(`UPDATE product_verifications SET status = 'stale', updated_at = datetime('now')
91
+ WHERE product_id = ? AND status IN ('issued','submitted','verified')`).run(productId);
92
+ return { invalidated: r.changes };
93
+ }
@@ -1,3 +1,4 @@
1
+ import { displayCurrency } from '../currency.js'; // agent-facing 币种统一 WAZ,遗留 'DCP' 读时归一化(绝不外泄 DCP)
1
2
  const BASE = 'https://webaz.xyz';
2
3
  // ACP availability 枚举(spec):in_stock | out_of_stock | pre_order | backorder | unknown
3
4
  function availability(stock) {
@@ -55,7 +56,7 @@ export function buildAcpProductFeed(db, opts = {}) {
55
56
  description: plainText(p.description, 5000), // spec: plain text, max 5000
56
57
  url: `${BASE}/#order-product/${p.id}`, // 商品详情页(SPA hash,200)
57
58
  // price: spec = Number + ISO 4217 code。WAZ 非 ISO 4217 → 见 feed 级 _disclosures.currency
58
- price: { amount: Number(p.price), currency: p.currency || 'WAZ' },
59
+ price: { amount: Number(p.price), currency: displayCurrency(p.currency) },
59
60
  availability: availability(p.stock),
60
61
  is_digital: p.product_type === 'digital',
61
62
  seller_name: (p.seller_name || p.seller_id).slice(0, 70),
@@ -106,7 +107,7 @@ export function buildAcpProductFeed(db, opts = {}) {
106
107
  },
107
108
  _disclosures: {
108
109
  phase: 'pre-launch',
109
- currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit (WAZ, or legacy 'DCP'), NOT an ISO 4217 fiat currency. No real money is involved.",
110
+ currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit, always emitted as WAZ (legacy internal-code rows are normalized to WAZ on read), NOT an ISO 4217 fiat currency. No real money is involved.",
110
111
  checkout: 'is_eligible_checkout is false for every item: ACP checkout is not yet wired. Products are DISCOVERABLE via ACP, not yet PURCHASABLE via ACP. Native WebAZ checkout + escrow + state machine govern real purchases (see RFC-015).',
111
112
  },
112
113
  product_count: products.length,
@@ -36,6 +36,9 @@ export const CONTRACT_CHANGES = [
36
36
  { contract_version: 3, date: '2026-06-09', surface: 'entity', kind: 'changed', summary: '§① order lifecycle: corrected declined_nofault state meaning text — it is NOT terminal (transitions declined_nofault→completed on settlement). Dropped the contradictory "(terminal)" label that conflicted with the auto-derived terminal:false. Semantics/state-machine unchanged; text-only clarification for agents reading the entity dictionary.' },
37
37
  { contract_version: 4, date: '2026-06-09', surface: 'capability', kind: 'changed', summary: '§② capability matrix: POST /api/reviews/:type/:id/claim now requires the new "review_claim" action scope instead of being SAFE (unscoped). The review-claim path locks a 5 WAZ stake (escrow), so it belongs under default-deny accountability like other value writes. GET reviews endpoints stay open.', migration: 'A declared agent that calls review claim must add the "review_claim" scope to its api_key (or hold a Passkey, or declare "*"). Passkey-bound humans and "*" agents are unaffected; GET reviews unchanged.' },
38
38
  { contract_version: 5, date: '2026-06-24', surface: 'integration', kind: 'added', summary: '§④ integration-contract entry (/.well-known/webaz-integration.json) gains an agent_quickstart block: a 60-second stranger-agent cold-start with discrete, machine-parseable fields (canonical_start_url, public_readonly_entrypoints, anonymous_allowed_actions, authenticated_required_actions, safe_next_actions, proposal_flow, contribution_boundary). Additive — no existing field changed; agents may ignore. NB: this surface is NOT covered by the §②/§① fingerprint, so it is registered here by hand.' },
39
+ { contract_version: 6, date: '2026-06-27', surface: 'entity', kind: 'added', summary: '§① order lifecycle gains two Direct Pay Rail 1 (non-custodial off-protocol payment) states: direct_pay_window (seller fee-stake locked, payment method shown, awaiting buyer off-protocol payment) and direct_expired_unconfirmed (payment window timed out without buyer confirmation — order is NOT silently closed; buyer retains a dispute/confirm window). Additive — existing order states/transitions unchanged; escrow orders never enter these states; agents may ignore unless they place direct_p2p orders.' },
40
+ { contract_version: 7, date: '2026-06-27', surface: 'capability', kind: 'added', summary: '§② capability matrix RESERVES a new "direct_pay" action scope for the future /api/direct-pay/* surface (Direct Pay Rail 1). SCAFFOLD ONLY: this revision ships the D1/D2 disclosure helper + append-only ack model and a Passkey RISK-guard helper for later wiring — NO production route is gated yet, and the current order-action paths (mark_paid / cancel / confirm / confirm-in-person) do NOT yet enforce the Passkey or the two-disclosure-ack gate. The classifier maps /api/direct-pay/* → direct_pay so that surface is RISK-scoped the moment routes are added. Additive — no existing action changed.', migration: 'No behavioural change yet — do NOT assume Direct Pay order actions are Passkey-gated or disclosure-gated at this revision, and do NOT assume agents are blocked from them. Real enforcement (Passkey + two-disclosure-ack on mark_paid/confirm/confirm-in-person) is wired in a later PR together with the create route, ack endpoints, and UI.' },
41
+ { contract_version: 8, date: '2026-07-02', surface: 'capability', kind: 'added', summary: 'Direct Pay Rail 1 (non-custodial) buyer account selection: POST /api/orders now accepts an OPTIONAL direct_receive_account_id — when a seller offers multiple receive accounts (GET /api/direct-receive/selectable-accounts, metadata-only), the buyer picks one and the create route snapshots THAT account. Omitted → unchanged dual-read fallback to the seller\'s single legacy payment instruction (no behaviour change for sellers with only that). The direct_p2p order gains a direct_pay_account_snapshot (non-sensitive metadata {account_id,method,currency,label,qr_ref} frozen at create); the raw instruction text stays in the existing disclosure-gated snapshot. New buyer read GET /api/orders/:id/direct-pay-qr serves the snapshotted receive-QR image bytes, gated to the order buyer AND both D1/D2 disclosure acks (non-enumerating 404 otherwise). Additive — this create-body field + order-metadata snapshot are NOT part of the §②/§① fingerprint projection, so registered here by hand; no existing action or order state changed.', migration: 'Agents placing direct_p2p orders MAY pass direct_receive_account_id (from selectable-accounts) to choose how they pay; omitting it preserves the prior single-instruction behaviour. The raw payment instruction and the QR image remain revealed only after both disclosure acks.' },
39
42
  ];
40
43
  export function buildChangeFeed() {
41
44
  return {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 直付 / 直接收款 RISK 门:
3
+ * ① 无 Passkey credential → 硬拒(agent 路径:agent 不可能持有 Passkey assertion → 永远过不了)。
4
+ * ② 需一次性真人 WebAuthn gate token(purpose-bound, 60s)。
5
+ * 两步都过才返回 ok。无自助批准路径。
6
+ */
7
+ export function requireDirectPayHumanPasskey(deps, args) {
8
+ const { db, consumeGateToken } = deps;
9
+ const { userId, webauthnToken, purpose, validate } = args;
10
+ // ① agent 硬拒:没有 Passkey 凭证的身份(agent)绝不能发起/批准直付
11
+ const hasPk = db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?').get(userId).n > 0;
12
+ if (!hasPk) {
13
+ return { ok: false, error_code: 'PASSKEY_REQUIRED_FOR_DIRECT_PAY', reason: '直付/直接收款是 RISK 操作:需绑定 Passkey 并真人二次确认,agent 不可代操作' };
14
+ }
15
+ // ② 一次性 purpose-bound 真人 WebAuthn gate token
16
+ const gate = consumeGateToken(userId, webauthnToken, purpose, validate ?? (() => true));
17
+ if (!gate.ok)
18
+ return { ok: false, error_code: 'HUMAN_PRESENCE_REQUIRED', reason: gate.reason || '需真实人工 WebAuthn 验证' };
19
+ return { ok: true };
20
+ }
@@ -0,0 +1,24 @@
1
+ import { requireBothDisclosuresAcked } from '../direct-pay-disclosures.js';
2
+ /** 买家自视角:未 both-acked 的 direct_p2p 订单,删收款目标(instruction 原文 + 账号快照 qr_ref)。就地改 o。 */
3
+ export function redactUnackedDirectPayTarget(db, o, userId) {
4
+ if (o.payment_rail !== 'direct_p2p' || o.buyer_id !== userId)
5
+ return;
6
+ if (requireBothDisclosuresAcked(db, o.id).ok)
7
+ return;
8
+ delete o.direct_pay_instruction_snapshot;
9
+ if (o.direct_pay_account_snapshot != null) {
10
+ try {
11
+ const s = JSON.parse(o.direct_pay_account_snapshot);
12
+ delete s.qr_ref;
13
+ o.direct_pay_account_snapshot = JSON.stringify(s);
14
+ }
15
+ catch {
16
+ delete o.direct_pay_account_snapshot;
17
+ }
18
+ }
19
+ }
20
+ /** 无条件删除收款目标(instruction 快照 + 整个账号快照)。用于非买家第三方 reader,他们绝不该看到收款目标。就地改 o。 */
21
+ export function stripDirectPayPaymentTarget(o) {
22
+ delete o.direct_pay_instruction_snapshot;
23
+ delete o.direct_pay_account_snapshot;
24
+ }
@@ -31,6 +31,9 @@ export const WRITE_RULES = [
31
31
  { method: 'POST', re: /^\/api\/skill-market\/[^/]+\/purchase/, action: 'purchase' },
32
32
  { method: 'POST', re: /^\/api\/secondhand\/[^/]+\/order/, action: 'buy_secondhand' },
33
33
  { method: 'POST', re: /^\/api\/group-buys\/[^/]+\/join/, action: 'group_buy_join' },
34
+ // Direct Pay (Rail 1) = RISK scope:为【未来】/api/direct-pay/* 写面【保留】'direct_pay' 分类(该面尚无路由)。
35
+ // ⚠️ SCAFFOLD:Passkey/两次披露门当前是 helper,【尚未】接到任何 handler;真实 enforcement 随 create-route/UI/ack 端点在后续 PR 落地。
36
+ { method: 'WRITE', re: /^\/api\/direct-pay\//, action: 'direct_pay' },
34
37
  // Codex #98 P1:review claim 锁 5 WAZ stake(扣 balance + escrowed)—— 资金写,绝不能落 SAFE,纳入 default-deny 问责门。
35
38
  // 只命中 .../:type/:id/claim;其余 reviews 写无规则 → 落通用 'write'(仍 default-deny),GET reviews 由 endpointToAction(GET) 返回 null。
36
39
  { method: 'POST', re: /^\/api\/reviews\/[^/]+\/[^/]+\/claim$/, action: 'review_claim' },