@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,321 @@
1
+ import { confirmProductionReceipt } from '../../direct-receive-deposits.js';
2
+ import { reviewAmlFlag } from '../../direct-pay-aml-review.js';
3
+ import { recordKybReview, recordSanctionsScreening, recordAmlFlagIngress, amlDetailHash } from '../../direct-pay-compliance-ingress.js';
4
+ import { readDirectPayLaunchReadiness } from '../../direct-pay-launch-readiness.js';
5
+ import { approveDeferral, rejectDeferral, listDeferrals } from '../../direct-receive-deferral.js';
6
+ import { listProductVerifications, reviewProductVerification } from '../../product-verification.js';
7
+ import { listStoreVerifications, reviewStoreVerification } from '../../store-verification.js';
8
+ import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
9
+ import { recordFeePrepayTopup, recordFeePrepayAdjustment, recordFeePrepayRefund, getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
10
+ export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
11
+ const { db, requireRootAdmin, consumeGateToken, logAdminAction, getProtocolParam } = deps;
12
+ // POST /api/admin/direct-receive/deposits/:id/confirm-production — ROOT + 真人 Passkey 手动确认生产保证金 receipt。
13
+ // 当前恒 fail-closed(无 legal-cleared rail → assert 抛 → PRODUCTION_RAIL_NOT_CLEARED)。
14
+ app.post('/api/admin/direct-receive/deposits/:id/confirm-production', (req, res) => {
15
+ const admin = requireRootAdmin(req, res);
16
+ if (!admin)
17
+ return;
18
+ const depositId = req.params.id;
19
+ const railId = String(req.body?.rail_id || '');
20
+ const amountUnits = Number(req.body?.expected_amount_units);
21
+ const receiptRef = String(req.body?.receipt_ref || '');
22
+ const jurisdiction = String(req.body?.jurisdiction || '');
23
+ const webauthnToken = req.body?.webauthn_token;
24
+ // 现场真人 Passkey(铁律)。purpose_data 绑定【完整动作字段】:deposit/rail/amount/receipt/jurisdiction 全等才放行,
25
+ // 防止把一次签名复用到另一笔/另一参数的确认上。
26
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
27
+ userId: admin.id, webauthnToken, purpose: 'direct_receive_production_confirm',
28
+ validate: (data) => {
29
+ const d = data;
30
+ return !!d && d.deposit_id === depositId && d.rail_id === railId && Number(d.amount_units) === amountUnits
31
+ && d.receipt_ref === receiptRef && d.jurisdiction === jurisdiction;
32
+ },
33
+ });
34
+ // 每次 ROOT 尝试都审计 —— 含【gate 失败】(生产保证金确认本身是敏感控制事件,缺 Passkey / 缺 token /
35
+ // purpose_data 不符的 ROOT 尝试同样要留痕)。非 ROOT 不审计(requireRootAdmin 已拦,无可信 admin 身份)。
36
+ if (!gate.ok) {
37
+ logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
38
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
39
+ }
40
+ // 写入只走唯一 writer。assert 抛 → 当前恒 fail-closed。每次 ROOT 尝试都审计(含结果)。
41
+ try {
42
+ const r = confirmProductionReceipt(db, { depositId, railId, expectedAmountUnits: amountUnits, receiptRef, jurisdiction });
43
+ logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'confirmed') : r.reason });
44
+ if (!r.ok)
45
+ return void res.status(409).json({ error: r.reason, error_code: 'PRODUCTION_CONFIRM_REJECTED' });
46
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
47
+ }
48
+ catch (e) {
49
+ // assertProductionDepositRail(及任何写前异常)→ 生产 rail 未 legal-clear → fail-closed。
50
+ logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: 'rail_not_cleared', error: e.message });
51
+ return void res.status(409).json({ error: '生产保证金收款轨未通过法务/生产放行,暂不可确认', error_code: 'PRODUCTION_RAIL_NOT_CLEARED' });
52
+ }
53
+ });
54
+ // POST /api/admin/direct-receive/aml-flags/:id/review — ROOT + 真人 Passkey 复核单条 AML flag (PR-6E)。
55
+ // route 只做 auth + gate + 参数校验 + 调 reviewAmlFlag(唯一 review writer,原子改 flag + 写 audit)。
56
+ app.post('/api/admin/direct-receive/aml-flags/:id/review', (req, res) => {
57
+ const admin = requireRootAdmin(req, res);
58
+ if (!admin)
59
+ return;
60
+ const flagId = req.params.id;
61
+ const decision = String(req.body?.decision || '');
62
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
63
+ const webauthnToken = req.body?.webauthn_token;
64
+ // 现场真人 Passkey(铁律)。purpose_data 绑 flag_id + decision:一次签名只用于该 flag 的该决策,杜绝复用。
65
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
66
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_aml_review',
67
+ validate: (data) => {
68
+ const d = data;
69
+ return !!d && d.flag_id === flagId && d.decision === decision;
70
+ },
71
+ });
72
+ if (!gate.ok) {
73
+ // gate 失败也审计(无 PII:flag_id + decision + 结果);成功复核的审计由 reviewAmlFlag 原子写入。
74
+ logAdminAction(admin.id, 'direct_pay.aml_review', 'aml_flag', flagId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
75
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
76
+ }
77
+ const r = reviewAmlFlag(db, { flagId, reviewerId: admin.id, decision, notes });
78
+ if (!r.ok) {
79
+ const status = r.error === 'FLAG_NOT_FOUND' ? 404 : 400;
80
+ return void res.status(status).json({ error: 'AML flag 复核失败', error_code: r.error });
81
+ }
82
+ return void res.json({ ok: true, flag_id: r.flagId, decision: r.decision, status: r.status, disposition: r.disposition });
83
+ });
84
+ // ── PR-6F 合规 ingress(受控写入入口)。三端点共用:ROOT + 真人 Passkey(purpose-bound)+ 调 ingress helper。 ──
85
+ // route 只做 auth + gate + 取参 + 调 helper(helper 原子写台账 + 审计);gate 失败也审计。无 vendor 外呼、无资金/状态机改动。
86
+ const str = (v) => v != null ? String(v) : '';
87
+ const opt = (v) => v != null ? String(v) : undefined;
88
+ // gatedIngress 返回一个 express handler(literal-path app.post 调用之 —— 让 API-doc 扫描器能识别端点)。
89
+ const gatedIngress = (purpose, makeValidate, run, failAudit) => (req, res) => {
90
+ const admin = requireRootAdmin(req, res);
91
+ if (!admin)
92
+ return;
93
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
94
+ userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose, validate: makeValidate(req),
95
+ });
96
+ if (!gate.ok) {
97
+ const fa = failAudit(req);
98
+ logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: gate.error_code });
99
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
100
+ }
101
+ const r = run(req, admin.id);
102
+ if (!r.ok) {
103
+ // 业务校验失败(Passkey 已过)也留痕:money-adjacent admin 尝试(如退款余额不足被拒)必须可审计。
104
+ const fa = failAudit(req);
105
+ logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: r.error });
106
+ return void res.status(400).json({ error: '合规 ingress 失败', error_code: r.error });
107
+ }
108
+ return void res.json({ ok: true, id: r.id });
109
+ };
110
+ // POST /api/admin/direct-receive/kyb-reviews — KYB 复核结论 ingress(ROOT + 真人 Passkey)。
111
+ // Passkey purpose_data 绑定【完整写入内容】(user_id+status+provider_ref+expires_at):签 A 写 B 一律拒。
112
+ app.post('/api/admin/direct-receive/kyb-reviews', gatedIngress('direct_pay_kyb_ingress', (req) => (data) => {
113
+ const d = data;
114
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
115
+ && str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
116
+ }, (req, adminId) => recordKybReview(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { kyb_status: str(req.body?.status) } })));
117
+ // POST /api/admin/direct-receive/sanctions-screenings — 制裁筛查结论 ingress(ROOT + 真人 Passkey;high-risk)。
118
+ // purpose_data 绑定 user_id+status+provider_ref+expires_at。
119
+ app.post('/api/admin/direct-receive/sanctions-screenings', gatedIngress('direct_pay_sanctions_ingress', (req) => (data) => {
120
+ const d = data;
121
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
122
+ && str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
123
+ }, (req, adminId) => recordSanctionsScreening(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { sanctions_status: str(req.body?.status) } })));
124
+ // POST /api/admin/direct-receive/aml-flags — AML flag ingress(ROOT + 真人 Passkey;high-risk)。与 .../aml-flags/:id/review 区分。
125
+ // purpose_data 绑定 user_id+rule+severity+status+related_order_id+detail_hash(canonical):签 A 写 B 一律拒。
126
+ const amlDetail = (req) => (req.body?.detail && typeof req.body.detail === 'object' && !Array.isArray(req.body.detail)) ? req.body.detail : undefined;
127
+ app.post('/api/admin/direct-receive/aml-flags', gatedIngress('direct_pay_aml_ingress', (req) => (data) => {
128
+ const d = data;
129
+ return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.rule) === str(req.body?.rule) && str(d.severity) === str(req.body?.severity)
130
+ && str(d.status) === str(req.body?.status) && str(d.related_order_id) === str(req.body?.related_order_id) && str(d.detail_hash) === amlDetailHash(amlDetail(req));
131
+ }, (req, adminId) => recordAmlFlagIngress(db, { userId: str(req.body?.user_id), reviewerId: adminId, rule: str(req.body?.rule), severity: str(req.body?.severity), status: str(req.body?.status), relatedOrderId: opt(req.body?.related_order_id), detail: amlDetail(req) }), (req) => ({ targetId: str(req.body?.user_id), detail: { rule: str(req.body?.rule), severity: str(req.body?.severity), aml_status: str(req.body?.status) } })));
132
+ // POST /api/admin/direct-receive/fee-prepay — 记录商家【平台服务费预付款】(ROOT + 真人 Passkey)。append-only;
133
+ // invoice_id NULL = 未分配预充值 → 计入 available_prepay(首单后续建单门)。purpose_data 绑定 seller_id+amount_units+method+evidence_ref。
134
+ // 不碰 buyer wallet/escrow/order/settlement/refund;非买家 escrow/保证金/penalty。本轮无"余额退款"(仅正向预付款登记)。
135
+ app.post('/api/admin/direct-receive/fee-prepay', gatedIngress('direct_pay_fee_prepay_record', (req) => (data) => {
136
+ const d = data;
137
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
138
+ && str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
139
+ }, (req, adminId) => recordFeePrepayTopup(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), note: opt(req.body?.note) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
140
+ // POST /api/admin/direct-receive/fee-adjust — 平台服务费账务【更正】(ROOT + 真人 Passkey)。带符号 delta;append-only + audit。
141
+ // ≠ 退款(不动真钱,只调记账)。purpose_data 绑 seller_id+delta_units+reason。
142
+ app.post('/api/admin/direct-receive/fee-adjust', gatedIngress('direct_pay_fee_adjust', (req) => (data) => {
143
+ const d = data;
144
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.delta_units) === Number(req.body?.delta_units) && str(d.reason) === str(req.body?.reason);
145
+ }, (req, adminId) => recordFeePrepayAdjustment(db, { sellerId: str(req.body?.seller_id), deltaUnits: Number(req.body?.delta_units), reason: str(req.body?.reason), recordedBy: adminId }), (req) => ({ targetId: str(req.body?.seller_id), detail: { delta_units: Number(req.body?.delta_units) } })));
146
+ // POST /api/admin/direct-receive/fee-refund — 平台服务费余额【退款】(ROOT + 真人 Passkey)。真实退还未消耗预付款;
147
+ // amount ≤ 当前 available(helper 同事务校验)。append-only + audit。purpose_data 绑 seller_id+amount_units+method+evidence_ref。
148
+ app.post('/api/admin/direct-receive/fee-refund', gatedIngress('direct_pay_fee_refund', (req) => (data) => {
149
+ const d = data;
150
+ return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
151
+ && str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
152
+ }, (req, adminId) => recordFeePrepayRefund(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), reason: opt(req.body?.reason) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
153
+ // GET /api/admin/direct-receive/fee-account/:seller_id — ROOT 只读:某商家平台服务费账户汇总(余额/预充值/应收/调整/退款/在途预估/宽限)。
154
+ // 只读诊断,不写、无 Passkey(读不授权能力);卖家私密财务,买家/卖家拿不到此 admin 视图。
155
+ app.get('/api/admin/direct-receive/fee-account/:seller_id', (req, res) => {
156
+ const admin = requireRootAdmin(req, res);
157
+ if (!admin)
158
+ return;
159
+ return void res.json({ ok: true, account: getDirectPayFeeAccount(db, str(req.params.seller_id)) });
160
+ });
161
+ // POST /api/admin/direct-receive/readiness — ROOT + 真人 Passkey:返回【完整】Direct Pay launch readiness(blockers + facts,
162
+ // 含 KYB/sanctions/AML/base-bond/rail clearance 全细节)。只读诊断(不写库、不 flip launch);ROOT 专用,买家/卖家拿不到。
163
+ app.post('/api/admin/direct-receive/readiness', (req, res) => {
164
+ const admin = requireRootAdmin(req, res);
165
+ if (!admin)
166
+ return;
167
+ const sellerId = req.body?.seller_id != null ? String(req.body.seller_id) : undefined;
168
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
169
+ userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_admin_readiness',
170
+ validate: (data) => { const d = data; return !!d && String(d.seller_id ?? '') === String(sellerId ?? ''); },
171
+ });
172
+ if (!gate.ok) {
173
+ logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: false, error_code: gate.error_code });
174
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
175
+ }
176
+ const readiness = readDirectPayLaunchReadiness(db, { getProtocolParam, sellerId });
177
+ logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: true, ready: readiness.ready, blocker_count: readiness.blockers.length });
178
+ return void res.json(readiness);
179
+ });
180
+ // ── 缓交(deferred base-bond)审批队列。读 = ROOT;批准/拒绝 = ROOT + 真人 Passkey(铁律,授予绝不自动)。 ──
181
+ // approveDeferral/rejectDeferral 是唯一 writer;本文件零 direct_receive_deferrals 直写。审批只改缓交状态机,
182
+ // 【不】碰 buyer wallet/escrow/order/settlement/refund/privileges,也不 flip launch。
183
+ const DEFERRAL_STATUSES = new Set(['pending', 'granted', 'rejected', 'expired']);
184
+ // GET /api/admin/direct-receive/deferrals?status=pending — ROOT 审批队列(默认全部;可按 status 过滤)。纯读。
185
+ app.get('/api/admin/direct-receive/deferrals', (req, res) => {
186
+ const admin = requireRootAdmin(req, res);
187
+ if (!admin)
188
+ return;
189
+ const status = req.query?.status != null ? String(req.query.status) : '';
190
+ if (status && !DEFERRAL_STATUSES.has(status))
191
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
192
+ return void res.json({ deferrals: listDeferrals(db, status ? { status: status } : {}) });
193
+ });
194
+ // POST /api/admin/direct-receive/deferrals/:id/approve — ROOT + 真人 Passkey 批准缓交。
195
+ // Passkey purpose_data 绑定【完整审批条款】(deferral_id + reduced_quota_factor + grace_days):签 A 批 B / 改条款一律拒。
196
+ app.post('/api/admin/direct-receive/deferrals/:id/approve', (req, res) => {
197
+ const admin = requireRootAdmin(req, res);
198
+ if (!admin)
199
+ return;
200
+ const deferralId = req.params.id;
201
+ const rqfRaw = req.body?.reduced_quota_factor;
202
+ const graceRaw = req.body?.grace_days;
203
+ const webauthnToken = req.body?.webauthn_token;
204
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
205
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_approve',
206
+ validate: (data) => {
207
+ const d = data;
208
+ return !!d && str(d.deferral_id) === deferralId && str(d.reduced_quota_factor) === str(rqfRaw) && str(d.grace_days) === str(graceRaw);
209
+ },
210
+ });
211
+ if (!gate.ok) {
212
+ logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
213
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
214
+ }
215
+ const r = approveDeferral(db, { deferralId, adminId: admin.id, nowIso: new Date().toISOString(),
216
+ graceDays: graceRaw != null ? Number(graceRaw) : undefined, reducedQuotaFactor: rqfRaw != null ? Number(rqfRaw) : undefined });
217
+ logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'granted') : r.reason });
218
+ if (!r.ok)
219
+ return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_APPROVE_REJECTED' });
220
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
221
+ });
222
+ // POST /api/admin/direct-receive/deferrals/:id/reject — ROOT + 真人 Passkey 拒绝缓交。purpose_data 绑 deferral_id。
223
+ app.post('/api/admin/direct-receive/deferrals/:id/reject', (req, res) => {
224
+ const admin = requireRootAdmin(req, res);
225
+ if (!admin)
226
+ return;
227
+ const deferralId = req.params.id;
228
+ const webauthnToken = req.body?.webauthn_token;
229
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
230
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_reject',
231
+ validate: (data) => { const d = data; return !!d && str(d.deferral_id) === deferralId; },
232
+ });
233
+ if (!gate.ok) {
234
+ logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
235
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
236
+ }
237
+ const r = rejectDeferral(db, { deferralId, adminId: admin.id });
238
+ logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'rejected') : r.reason });
239
+ if (!r.ok)
240
+ return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_REJECT_REJECTED' });
241
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
242
+ });
243
+ // ── 按产品认证(per-product verification)审批。读 = ROOT;verify/reject = ROOT + 真人 Passkey(硬门:核验=放行该产品直付,
244
+ // capability-granting → 铁律 Passkey)。reviewProductVerification 是唯一 writer;本文件零 product_verifications 直写。 ──
245
+ const PV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
246
+ // GET /api/admin/direct-receive/product-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
247
+ app.get('/api/admin/direct-receive/product-verifications', (req, res) => {
248
+ const admin = requireRootAdmin(req, res);
249
+ if (!admin)
250
+ return;
251
+ const status = req.query?.status != null ? String(req.query.status) : '';
252
+ if (status && !PV_STATUSES.has(status))
253
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
254
+ return void res.json({ verifications: listProductVerifications(db, status ? { status: status } : {}) });
255
+ });
256
+ // POST /api/admin/direct-receive/product-verifications/:id/review — ROOT + 真人 Passkey 手动核对结论(verified|rejected)。
257
+ // Passkey purpose_data 绑 verification_id + decision:签 A 用 B / 改结论一律拒。verify = 放行该产品直付(逐品,绝不放行全部)。
258
+ app.post('/api/admin/direct-receive/product-verifications/:id/review', (req, res) => {
259
+ const admin = requireRootAdmin(req, res);
260
+ if (!admin)
261
+ return;
262
+ const verificationId = req.params.id;
263
+ const decision = String(req.body?.decision || '');
264
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
265
+ const webauthnToken = req.body?.webauthn_token;
266
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
267
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_product_verify',
268
+ validate: (data) => { const d = data; return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision; },
269
+ });
270
+ if (!gate.ok) {
271
+ logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
272
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
273
+ }
274
+ const r = reviewProductVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, notes });
275
+ logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
276
+ if (!r.ok)
277
+ return void res.status(409).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_REVIEW_REJECTED' });
278
+ return void res.json({ ok: true, status: r.status, already: !!r.already });
279
+ });
280
+ // ── 店铺认证(per-seller)审批 = 逐品验证豁免决定。读 = ROOT;verify/reject = ROOT + 真人 Passkey。 ──
281
+ // 核店铺时勾选 per_product_exempt:true → 该卖家所有商品免逐品验证(capability-granting → 铁律 Passkey;
282
+ // purpose_data 绑 verification_id+decision+per_product_exempt,签 A 用 B / 改豁免位一律拒)。reviewStoreVerification 唯一 writer。
283
+ const SV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
284
+ // GET /api/admin/direct-receive/store-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
285
+ app.get('/api/admin/direct-receive/store-verifications', (req, res) => {
286
+ const admin = requireRootAdmin(req, res);
287
+ if (!admin)
288
+ return;
289
+ const status = req.query?.status != null ? String(req.query.status) : '';
290
+ if (status && !SV_STATUSES.has(status))
291
+ return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
292
+ return void res.json({ verifications: listStoreVerifications(db, status ? { status: status } : {}) });
293
+ });
294
+ // POST /api/admin/direct-receive/store-verifications/:id/review — ROOT + 真人 Passkey 核店铺 + 勾选逐品豁免。
295
+ app.post('/api/admin/direct-receive/store-verifications/:id/review', (req, res) => {
296
+ const admin = requireRootAdmin(req, res);
297
+ if (!admin)
298
+ return;
299
+ const verificationId = req.params.id;
300
+ const decision = String(req.body?.decision || '');
301
+ const perProductExempt = req.body?.per_product_exempt === true;
302
+ const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
303
+ const webauthnToken = req.body?.webauthn_token;
304
+ const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
305
+ userId: admin.id, webauthnToken, purpose: 'direct_pay_store_verify',
306
+ validate: (data) => {
307
+ const d = data;
308
+ return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision && (d.per_product_exempt === true) === perProductExempt;
309
+ },
310
+ });
311
+ if (!gate.ok) {
312
+ logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
313
+ return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
314
+ }
315
+ const r = reviewStoreVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, perProductExempt, notes });
316
+ logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
317
+ if (!r.ok)
318
+ return void res.status(409).json({ error: r.reason, error_code: 'STORE_VERIFICATION_REVIEW_REJECTED' });
319
+ return void res.json({ ok: true, status: r.status, per_product_exempt: !!r.perProductExempt, already: !!r.already });
320
+ });
321
+ }
@@ -0,0 +1,255 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
3
+ import { initAgentDelegationGrantsSchema, initAgentPairingSchema, initAgentGrantAuthLogSchema } from '../../runtime/webaz-schema-helpers.js';
4
+ import { validateRequestedCapabilities, clampTtlSeconds, grantIsActive } from '../../runtime/agent-grant-scopes.js';
5
+ import { generateUserCode, verifyPkceS256, clampPairingTtlSeconds, pairingApprovable, pairingRetrievable } from '../../runtime/agent-pairing.js';
6
+ import { verifyGrantToken } from '../../runtime/agent-grant-verifier.js';
7
+ // Bounds on a pairing request (anti-bloat for the anonymous start endpoint).
8
+ const MAX_CAPABILITIES = 12;
9
+ const MAX_CONSTRAINTS_JSON = 2000;
10
+ function safeParseCaps(json) {
11
+ try {
12
+ return JSON.parse(String(json));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
18
+ /** Server-generated consent view for a pairing — canonical scope labels only, no secrets. */
19
+ function consentView(p) {
20
+ return {
21
+ pairing_id: p.pairing_id,
22
+ agent_label: p.agent_label || null,
23
+ reason: p.reason || null, // agent-supplied free text (display only)
24
+ capabilities: safeParseCaps(p.capabilities), // server-validated safe scopes
25
+ status: p.status,
26
+ expires_at: p.expires_at,
27
+ notice: 'Approving issues a scoped, short-lived, revocable delegation grant — NOT your api_key, NOT your funds. Safe (read/draft) scopes only; it can never move money, vote, arbitrate, or change keys.',
28
+ };
29
+ }
30
+ export function registerAgentGrantsRoutes(app, deps) {
31
+ const { db, auth, generateId, rateLimitOk } = deps;
32
+ // PWA runtime self-init (MCP gets the tables via applyWebazRuntimeSchema). Idempotent.
33
+ initAgentDelegationGrantsSchema(db);
34
+ initAgentPairingSchema(db);
35
+ initAgentGrantAuthLogSchema(db);
36
+ // ─────────────────────────── RFC-020 PR-C2a: opt-in grant-scope enforcement ───────────────────────────
37
+ // EXPLICIT, per-route, per-SAFE-scope. NOT global auth — a gtk_* token is accepted ONLY by routes that
38
+ // deliberately mount requireAgentGrantScope(scope); auth()/api_key is untouched and never accepts gtk_*.
39
+ // Risk / never-delegable scopes can never pass (the verifier hard-fails non-safe required scopes).
40
+ const requireAgentGrantScope = (scope) => async (req, res, next) => {
41
+ // Anti-abuse: throttle the grant-consumption path BEFORE any DB work (parity with pair/start).
42
+ // Bounds both anonymous probing and valid-grant spam, and caps audit-log growth.
43
+ if (!rateLimitOk(`agent_grant:${req.ip || 'anon'}`, 30, 60_000)) {
44
+ return void res.status(429).json({ error: 'too_many_grant_requests', error_code: 'GRANT_RATE_LIMITED', retry_after_s: 60 });
45
+ }
46
+ const bearer = (req.header('authorization') || '').replace(/^Bearer\s+/i, '');
47
+ const presentedGrant = bearer.startsWith('gtk_'); // a request that presents no grant bearer isn't "grant-authorized"
48
+ const r = await verifyGrantToken(bearer, scope);
49
+ // Append-only audit (RFC-020 §3.7 + invariant: every grant-authorized request is audited). Only audit
50
+ // requests that actually presented a grant bearer — a no-token request is pure noise (and an unauth
51
+ // bloat vector), not a grant-authorized request.
52
+ let audited = false;
53
+ if (presentedGrant) {
54
+ try {
55
+ await dbRun('INSERT INTO agent_grant_auth_log (grant_id, human_id, capability, outcome, error_code) VALUES (?,?,?,?,?)', [r.ok ? r.principal.grant_id : (r.grant_id ?? null), r.ok ? r.principal.human_id : (r.human_id ?? null), scope, r.ok ? 'allow' : 'deny', r.ok ? null : r.error_code]);
56
+ audited = true;
57
+ }
58
+ catch (e) {
59
+ console.error('[agent-grant] audit write failed:', e.message);
60
+ }
61
+ }
62
+ // Deny path: return the denial regardless of audit (no access is granted, so nothing to fail closed on).
63
+ if (!r.ok)
64
+ return void res.status(r.status).json({ error: r.error, error_code: r.error_code });
65
+ // Success path: FAIL CLOSED if the authorization could not be audited — a grant-authorized request
66
+ // must never proceed unaudited (RFC-020 invariant). Better to deny (503, retryable) than act unaccountably.
67
+ if (!audited)
68
+ return void res.status(503).json({ error: 'authorization audit unavailable; refusing to proceed unaudited', error_code: 'GRANT_AUDIT_FAILED' });
69
+ req.agentGrant = r.principal;
70
+ next();
71
+ };
72
+ // Vertical slice (zero-risk): grant principal introspection. Proves the verifier + opt-in middleware
73
+ // end-to-end on a brand-new read-only endpoint that touches NO existing route and NO money path.
74
+ app.get('/api/agent-grants/whoami', requireAgentGrantScope('read_public'), (req, res) => {
75
+ const p = req.agentGrant;
76
+ res.json({ grant: p, note: 'Authorized via delegation grant (safe scope read_public). This is a grant principal, not a human session.' });
77
+ });
78
+ // ─────────────────────────── RFC-020 PR-C1: pairing (device-flow + PKCE) ───────────────────────────
79
+ // C1 = pairing + credential delivery ONLY. No grant is consumed by any tool here (that is PR-C2).
80
+ // (pair 1) Agent starts a pairing — UNAUTHENTICATED (agent has no credential yet). Safe scopes only.
81
+ app.post('/api/agent-grants/pair/start', async (req, res) => {
82
+ // Rate-limit the anonymous write FIRST (anti-bloat: no DB row unless under the cap).
83
+ if (!rateLimitOk(`agent_pair_start:${req.ip || 'anon'}`, 10, 60_000)) {
84
+ return void res.status(429).json({ error: 'too_many_pairing_starts', retry_after_s: 60 });
85
+ }
86
+ const body = (req.body || {});
87
+ const codeChallenge = typeof body.code_challenge === 'string' ? body.code_challenge : '';
88
+ if (!codeChallenge || codeChallenge.length < 32 || codeChallenge.length > 256)
89
+ return void res.status(400).json({ error: 'code_challenge required (PKCE S256)' });
90
+ const caps = Array.isArray(body.capabilities) ? body.capabilities : [];
91
+ if (caps.length > MAX_CAPABILITIES)
92
+ return void res.status(400).json({ error: 'too_many_capabilities', max: MAX_CAPABILITIES });
93
+ const v = validateRequestedCapabilities(caps);
94
+ if (!v.ok)
95
+ return void res.status(403).json({ error: 'pairing_rejected', rejected: v.rejected }); // risk + never-delegable hard-reject
96
+ const pairingId = generateId('par');
97
+ const userCode = generateUserCode();
98
+ const ttl = clampPairingTtlSeconds(body.ttl_seconds);
99
+ const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
100
+ const label = typeof body.agent_label === 'string' ? body.agent_label.slice(0, 120) : null;
101
+ const reason = typeof body.reason === 'string' ? body.reason.slice(0, 280) : null; // agent free text only
102
+ const pubkey = typeof body.agent_pubkey === 'string' ? body.agent_pubkey.slice(0, 1000) : null; // RESERVED (PoP), not verified in C1
103
+ const capsJson = JSON.stringify(v.safe.map(c => ({ capability: c, constraints: (caps.find(x => x?.capability === c)?.constraints) || {} })));
104
+ if (capsJson.length > MAX_CONSTRAINTS_JSON)
105
+ return void res.status(400).json({ error: 'capabilities_too_large', max_bytes: MAX_CONSTRAINTS_JSON });
106
+ await dbRun('INSERT INTO agent_pairing_sessions (pairing_id, user_code, code_challenge, agent_label, agent_pubkey, reason, capabilities, status, expires_at) VALUES (?,?,?,?,?,?,?,?,?)', [pairingId, userCode, codeChallenge, label, pubkey, reason, capsJson, 'pending', expiresAt]);
107
+ res.status(201).json({
108
+ pairing_id: pairingId,
109
+ user_code: userCode,
110
+ approve_url: `/#pair?code=${userCode}`,
111
+ expires_at: expiresAt,
112
+ note: 'Ask the human to open approve_url at webaz.xyz (logged in) and approve. Then retrieve the credential with the PKCE verifier.',
113
+ });
114
+ });
115
+ // (pair 2) Human reviews the server-generated consent — human-authenticated.
116
+ app.get('/api/agent-grants/pair/:user_code', async (req, res) => {
117
+ const user = auth(req, res);
118
+ if (!user)
119
+ return;
120
+ const p = await dbOne('SELECT * FROM agent_pairing_sessions WHERE user_code = ?', [req.params.user_code]);
121
+ if (!p)
122
+ return void res.status(404).json({ error: 'pairing_not_found' });
123
+ if (!pairingApprovable(p, new Date().toISOString()))
124
+ return void res.status(409).json({ error: 'pairing_not_pending_or_expired', status: p.status });
125
+ res.json({ consent: consentView(p) });
126
+ });
127
+ // (pair 3) Human approves — human-authenticated. Issues the grant (token_hash filled at retrieve).
128
+ app.post('/api/agent-grants/pair/:user_code/approve', async (req, res) => {
129
+ const user = auth(req, res);
130
+ if (!user)
131
+ return;
132
+ const now = new Date().toISOString();
133
+ const p = await dbOne('SELECT * FROM agent_pairing_sessions WHERE user_code = ?', [req.params.user_code]);
134
+ if (!p)
135
+ return void res.status(404).json({ error: 'pairing_not_found' });
136
+ if (!pairingApprovable(p, now))
137
+ return void res.status(409).json({ error: 'pairing_not_pending_or_expired', status: p.status });
138
+ // Re-validate scopes at approval time (defense in depth) — must still be safe-only.
139
+ const caps = safeParseCaps(p.capabilities);
140
+ const v = validateRequestedCapabilities(caps);
141
+ if (!v.ok)
142
+ return void res.status(403).json({ error: 'pairing_rejected', rejected: v.rejected });
143
+ const grantId = generateId('grt');
144
+ const expiresAt = new Date(Date.now() + clampTtlSeconds(undefined) * 1000).toISOString();
145
+ // Grant created WITHOUT a token (token_hash NULL) — the bearer is minted only at retrieval.
146
+ await dbRun('INSERT INTO agent_delegation_grants (grant_id, human_id, agent_label, capabilities, token_hash, human_confirm_required, status, expires_at) VALUES (?,?,?,?,?,?,?,?)', [grantId, user.id, p.agent_label || null, JSON.stringify(caps), null, 0, 'active', expiresAt]);
147
+ await dbRun("UPDATE agent_pairing_sessions SET status='approved', human_id=?, grant_id=?, approved_at=? WHERE user_code=? AND status='pending'", [user.id, grantId, now, req.params.user_code]);
148
+ res.json({ success: true, grant_id: grantId, capabilities: caps });
149
+ });
150
+ // (pair 4) Agent retrieves the credential ONCE via PKCE verifier — UNAUTHENTICATED (PKCE-gated).
151
+ app.post('/api/agent-grants/pair/:pairing_id/retrieve', async (req, res) => {
152
+ const now = new Date().toISOString();
153
+ const verifier = typeof req.body?.code_verifier === 'string' ? req.body.code_verifier : '';
154
+ const p = await dbOne('SELECT * FROM agent_pairing_sessions WHERE pairing_id = ?', [req.params.pairing_id]);
155
+ if (!p)
156
+ return void res.status(404).json({ error: 'pairing_not_found' });
157
+ if (p.status === 'consumed' || p.consumed_at)
158
+ return void res.status(409).json({ error: 'pairing_already_consumed' });
159
+ if (!pairingRetrievable(p, now))
160
+ return void res.status(409).json({ error: 'pairing_not_approved_or_expired', status: p.status });
161
+ if (!verifyPkceS256(verifier, String(p.code_challenge)))
162
+ return void res.status(403).json({ error: 'pkce_mismatch' });
163
+ // Confirm the issued grant is still active (could have been revoked between approve and retrieve).
164
+ const grant = await dbOne('SELECT grant_id, status, capabilities, expires_at FROM agent_delegation_grants WHERE grant_id = ?', [String(p.grant_id)]);
165
+ if (!grant || grant.status !== 'active')
166
+ return void res.status(409).json({ error: 'grant_inactive' });
167
+ // Mint the bearer ONCE here; persist only its SHA-256 hash. Raw bearer is returned a single time.
168
+ const token = `gtk_${randomBytes(32).toString('hex')}`;
169
+ const tokenHash = createHash('sha256').update(token).digest('hex');
170
+ // One-time consume: only succeeds if still approved+unconsumed (guards against retrieval races/reuse).
171
+ const consumed = await dbRun("UPDATE agent_pairing_sessions SET status='consumed', consumed_at=? WHERE pairing_id=? AND status='approved' AND consumed_at IS NULL", [now, req.params.pairing_id]);
172
+ if (!consumed || consumed.changes !== 1)
173
+ return void res.status(409).json({ error: 'pairing_already_consumed' });
174
+ await dbRun('UPDATE agent_delegation_grants SET token_hash=? WHERE grant_id=?', [tokenHash, grant.grant_id]);
175
+ res.json({
176
+ grant_id: grant.grant_id,
177
+ token, // shown ONCE — agent stores it locally; server keeps only the hash
178
+ token_note: 'Shown once. Store in your OS secret store; the server keeps only a hash and cannot reissue it.',
179
+ capabilities: safeParseCaps(grant.capabilities),
180
+ expires_at: grant.expires_at,
181
+ });
182
+ });
183
+ // ── Issue a grant (human-authenticated). Safe scopes only; risk/never-delegable rejected. ──
184
+ app.post('/api/agent-grants', async (req, res) => {
185
+ const user = auth(req, res);
186
+ if (!user)
187
+ return;
188
+ const body = (req.body || {});
189
+ const caps = Array.isArray(body.capabilities) ? body.capabilities : [];
190
+ const v = validateRequestedCapabilities(caps);
191
+ if (!v.ok) {
192
+ // Fail-closed: any risk / never-delegable / unknown scope rejects the whole request.
193
+ return void res.status(403).json({ error: 'grant_rejected', rejected: v.rejected });
194
+ }
195
+ const ttl = clampTtlSeconds(body.ttl_seconds);
196
+ const grantId = generateId('grt');
197
+ const token = `gtk_${randomBytes(32).toString('hex')}`; // bearer — shown once
198
+ const tokenHash = createHash('sha256').update(token).digest('hex');
199
+ const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
200
+ const label = typeof body.agent_label === 'string' ? body.agent_label.slice(0, 120) : null;
201
+ const capsJson = JSON.stringify(v.safe.map(c => ({
202
+ capability: c,
203
+ constraints: (caps.find(x => x?.capability === c)?.constraints) || {},
204
+ })));
205
+ await dbRun('INSERT INTO agent_delegation_grants (grant_id, human_id, agent_label, capabilities, token_hash, human_confirm_required, status, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [grantId, user.id, label, capsJson, tokenHash, 0, 'active', expiresAt]);
206
+ res.status(201).json({
207
+ grant_id: grantId,
208
+ token,
209
+ token_note: 'Shown once — store securely. The server keeps only a hash; it cannot show this again.',
210
+ capabilities: JSON.parse(capsJson),
211
+ expires_at: expiresAt,
212
+ note: 'Bearer-first grant for safe scopes only. Risk scopes are not delegable until their route has a live-Passkey gate; PoP binding is required before any risk scope or longer-lived delegation (RFC-020).',
213
+ });
214
+ });
215
+ // ── Read: the human's connected agents (no secrets) + recent-use from the audit log (PR-D). ──
216
+ // last_used_at / use_count come from agent_grant_auth_log (RFC-020 §3.7) — the data the
217
+ // "Connected agents" UI shows so a human can spot stale/unused or busy agents before revoking.
218
+ app.get('/api/agent-grants', async (req, res) => {
219
+ const user = auth(req, res);
220
+ if (!user)
221
+ return;
222
+ const rows = await dbAll(`SELECT g.grant_id, g.agent_label, g.capabilities, g.status, g.created_at, g.expires_at, g.revoked_at, g.revoked_reason,
223
+ MAX(CASE WHEN l.outcome = 'allow' THEN l.ts END) AS last_used_at,
224
+ COUNT(CASE WHEN l.outcome = 'allow' THEN 1 END) AS use_count
225
+ FROM agent_delegation_grants g
226
+ LEFT JOIN agent_grant_auth_log l ON l.grant_id = g.grant_id
227
+ WHERE g.human_id = ?
228
+ GROUP BY g.grant_id
229
+ ORDER BY g.created_at DESC`, [user.id]);
230
+ const now = new Date().toISOString();
231
+ res.json({
232
+ grants: rows.map(g => ({
233
+ ...g,
234
+ capabilities: safeParseCaps(g.capabilities),
235
+ use_count: Number(g.use_count) || 0,
236
+ active: grantIsActive(g, now),
237
+ })),
238
+ });
239
+ });
240
+ // ── Revoke (online, one-click). ──
241
+ app.post('/api/agent-grants/:grant_id/revoke', async (req, res) => {
242
+ const user = auth(req, res);
243
+ if (!user)
244
+ return;
245
+ const grantId = req.params.grant_id;
246
+ const g = await dbOne('SELECT grant_id, status FROM agent_delegation_grants WHERE grant_id = ? AND human_id = ?', [grantId, user.id]);
247
+ if (!g)
248
+ return void res.status(404).json({ error: 'grant_not_found' });
249
+ if (g.status === 'revoked')
250
+ return void res.json({ success: true, already_revoked: true, grant_id: grantId });
251
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason.slice(0, 200) : null;
252
+ await dbRun("UPDATE agent_delegation_grants SET status = 'revoked', revoked_at = ?, revoked_reason = ? WHERE grant_id = ? AND human_id = ?", [new Date().toISOString(), reason, grantId, user.id]);
253
+ res.json({ success: true, grant_id: grantId });
254
+ });
255
+ }
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerBuyerFeedsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
5
  const { auth, isTrustedRole, errorRes, getNearbyCellPrecision, getProtocolParam } = deps;
@@ -28,7 +29,7 @@ export function registerBuyerFeedsRoutes(app, deps) {
28
29
  ? `AND p.id NOT IN (${exclArgs.map(() => '?').join(',')})`
29
30
  : '';
30
31
  const baseCols = `p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.seller_id,
31
- (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count,
32
+ (SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count,
32
33
  u.name as seller_name, u.handle as seller_handle`;
33
34
  let followedProducts = [];
34
35
  if (followedSellerIds.length > 0) {
@@ -108,7 +108,12 @@ export function registerChatRoutes(app, deps) {
108
108
  CASE WHEN c.user_a = ? THEN c.unread_a ELSE c.unread_b END as my_unread,
109
109
  CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END as other_id,
110
110
  (SELECT handle FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_handle,
111
- (SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name
111
+ (SELECT name FROM users WHERE id = CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END) as other_name,
112
+ -- 友好标签:订单/问商品 → 商品名,让收件箱里同一对方的多个会话可区分(不只显示 kind)。无新 bind 参数(仅用 c.kind/c.context_id)。
113
+ (CASE c.kind
114
+ WHEN 'order' THEN (SELECT p.title FROM orders o JOIN products p ON p.id = o.product_id WHERE o.id = c.context_id)
115
+ WHEN 'listing_qa' THEN (SELECT title FROM products WHERE id = c.context_id)
116
+ ELSE NULL END) as context_title
112
117
  FROM conversations c
113
118
  WHERE (c.user_a = ? OR c.user_b = ?) AND c.status NOT IN ('blocked','archived')
114
119
  ORDER BY COALESCE(c.last_message_at, c.created_at) DESC
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // RFC-018 PR4: 真实成交(排除全额退货)
2
3
  export function registerDashboardsRoutes(app, deps) {
3
4
  // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
4
5
  const { auth } = deps;
@@ -70,7 +71,7 @@ export function registerDashboardsRoutes(app, deps) {
70
71
  `, [userId, userId, userId, userId, userId, userId]);
71
72
  const highComm = await dbAll(`
72
73
  SELECT p.id, p.title, p.price, p.commission_rate, p.images, p.category,
73
- (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
74
+ (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND ${genuineSalePredicate('o')}) as sales_count
74
75
  FROM products p
75
76
  WHERE p.status = 'active'
76
77
  AND p.commission_rate > 0