@seasonkoh/webaz 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/layer1-agent/L1-1-mcp-server/server.js +138 -2
- package/dist/pwa/public/app.js +173 -7
- package/dist/pwa/public/i18n.js +26 -0
- package/dist/pwa/public/sw.js +1 -1
- package/dist/pwa/routes/agent-governance.js +51 -6
- package/dist/pwa/routes/ap2-mandate.js +123 -0
- package/dist/pwa/routes/auth-register.js +29 -2
- package/dist/pwa/routes/checkout-helpers.js +23 -2
- package/dist/pwa/routes/orders-create.js +46 -3
- package/dist/pwa/routes/public-utils.js +60 -3
- package/dist/pwa/server.js +74 -1
- package/package.json +1 -1
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema,
|
|
17
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, // #B.1 a — MCP 三大原语之 Prompts
|
|
18
|
+
GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
19
|
import { initDatabase, generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
19
20
|
import { transition, getOrderStatus, initSystemUser, } from '../../layer0-foundation/L0-2-state-machine/engine.js';
|
|
20
21
|
import { initDisputeSchema, createDispute, respondToDispute, getDisputeDetails, getOrderDispute, getOpenDisputes, } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
|
|
@@ -3925,7 +3926,7 @@ function settleOrder(db, orderId) {
|
|
|
3925
3926
|
}
|
|
3926
3927
|
// ─── MCP Server 主体 ──────────────────────────────────────────
|
|
3927
3928
|
export async function startMCPServer() {
|
|
3928
|
-
const server = new Server({ name: 'dcp-protocol', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
3929
|
+
const server = new Server({ name: 'dcp-protocol', version: '0.1.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
3929
3930
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
3930
3931
|
// ── MCP Resources:协议 Manifest ─────────────────────────────
|
|
3931
3932
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
@@ -3953,6 +3954,141 @@ export async function startMCPServer() {
|
|
|
3953
3954
|
],
|
|
3954
3955
|
};
|
|
3955
3956
|
});
|
|
3957
|
+
// ── MCP Prompts:预定义对话模板(#B.1 a — MCP 三大原语补齐)─────────────
|
|
3958
|
+
// Claude Desktop / Cursor / 其它 MCP 客户端会把这些 prompt 作为推荐对话起点呈现给用户。
|
|
3959
|
+
// 每个 prompt = 一个"如何在 webaz 完成 X"的引导,用户选了之后,模型按 prompt 模板开始对话。
|
|
3960
|
+
// 加这一层让 onboarding 极大简化 — 用户不需要先读 webaz_info 再决定怎么用。
|
|
3961
|
+
const PROMPTS = [
|
|
3962
|
+
{
|
|
3963
|
+
name: 'webaz-place-order',
|
|
3964
|
+
description: '引导买家 agent 完成「发现 → 验价 → 锁价 → 下单」全流程(含粘贴外链精准匹配)。如果用户给你商品链接,用这个 prompt。',
|
|
3965
|
+
arguments: [
|
|
3966
|
+
{ name: 'user_intent', description: '用户原始需求或粘贴的链接文本', required: true },
|
|
3967
|
+
],
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
name: 'webaz-list-product',
|
|
3971
|
+
description: '引导卖家 agent 完成商品上架 — 含 SEO/Agent 友好度最佳实践(填全 brand/model/specs/return_days/handling_hours 等 Schema.org 加分字段)。',
|
|
3972
|
+
arguments: [
|
|
3973
|
+
{ name: 'product_summary', description: '卖家口述的商品概要', required: true },
|
|
3974
|
+
],
|
|
3975
|
+
},
|
|
3976
|
+
{
|
|
3977
|
+
name: 'webaz-onboard',
|
|
3978
|
+
description: '新 agent 第一次接入 webaz 时的引导 — 解释协议性质 / pre-launch 状态 / MLM 形态 / 注册路径 / 用户授权边界。先读 webaz_info,再走这个 prompt。',
|
|
3979
|
+
arguments: [],
|
|
3980
|
+
},
|
|
3981
|
+
{
|
|
3982
|
+
name: 'webaz-handle-dispute',
|
|
3983
|
+
description: '订单出现问题时引导处理 — 区分"协商退款 / 走争议仲裁 / 卖家主动取消"三条路径,提示铁律(arbitrate 必须 PWA + Passkey)。',
|
|
3984
|
+
arguments: [
|
|
3985
|
+
{ name: 'order_id', description: '出问题的订单 ID', required: true },
|
|
3986
|
+
{ name: 'issue_summary', description: '问题描述(收到货不符 / 没收到 / 质量问题等)', required: true },
|
|
3987
|
+
],
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
name: 'webaz-cross-border',
|
|
3991
|
+
description: '中国跨境卖家参与引导 — 国内用 webaz 自有协议 / 跨境暴露 UCP merchant endpoint 给全球 agent。讲清双轨架构 + 合规边界。',
|
|
3992
|
+
arguments: [],
|
|
3993
|
+
},
|
|
3994
|
+
];
|
|
3995
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
|
|
3996
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
3997
|
+
const name = request.params.name;
|
|
3998
|
+
const args = (request.params.arguments || {});
|
|
3999
|
+
// 每个 prompt 返回一份 message 列表,客户端把它当作对话起点
|
|
4000
|
+
const messages = [];
|
|
4001
|
+
switch (name) {
|
|
4002
|
+
case 'webaz-place-order':
|
|
4003
|
+
messages.push({
|
|
4004
|
+
role: 'user',
|
|
4005
|
+
content: {
|
|
4006
|
+
type: 'text',
|
|
4007
|
+
text: `用户需求:${args.user_intent || '(未提供)'}\n\n` +
|
|
4008
|
+
`请使用 webaz_search 工具(可粘贴外链精准匹配,详见工具描述)找到最匹配的商品,` +
|
|
4009
|
+
`然后用 webaz_verify_price 锁价(返回 session_token,10 分钟有效),` +
|
|
4010
|
+
`最后用 webaz_place_order(session_token=...) 下单。` +
|
|
4011
|
+
`下单时记得跟用户确认收货地址 + 是否需要使用推荐人 promoter_api_key(可选)。\n\n` +
|
|
4012
|
+
`重要:不要跳过 verify_price 这一步 — 它是防价格篡改的关键。`,
|
|
4013
|
+
},
|
|
4014
|
+
});
|
|
4015
|
+
break;
|
|
4016
|
+
case 'webaz-list-product':
|
|
4017
|
+
messages.push({
|
|
4018
|
+
role: 'user',
|
|
4019
|
+
content: {
|
|
4020
|
+
type: 'text',
|
|
4021
|
+
text: `卖家概要:${args.product_summary || '(未提供)'}\n\n` +
|
|
4022
|
+
`请使用 webaz_list_product 工具上架。最佳实践:**填得越完整,SEO + agent 命中精度越高**:\n` +
|
|
4023
|
+
`- 必填:title / description / price\n` +
|
|
4024
|
+
`- 高 SEO 价值:images / category / brand / model / return_days(>0) / handling_hours / ship_regions\n` +
|
|
4025
|
+
`- 中等价值:specs(JSON 结构化,如 {颜色:黑色,容量:350ml}) / warranty_days / weight_kg / return_condition\n` +
|
|
4026
|
+
`- 跨境(可选):source_price(外部参考价) + 多语言版本(通过 PWA 上架)\n\n` +
|
|
4027
|
+
`Schema.org Product / Offer / shippingDetails / hasMerchantReturnPolicy 字段已自动映射 —` +
|
|
4028
|
+
`Google Rich Result 和外部 agent 会直接读到这些字段。`,
|
|
4029
|
+
},
|
|
4030
|
+
});
|
|
4031
|
+
break;
|
|
4032
|
+
case 'webaz-onboard':
|
|
4033
|
+
messages.push({
|
|
4034
|
+
role: 'user',
|
|
4035
|
+
content: {
|
|
4036
|
+
type: 'text',
|
|
4037
|
+
text: `请先调用 webaz_info 拿到完整协议状态,然后跟我确认以下几点理解:\n\n` +
|
|
4038
|
+
`1. **网络阶段(network_state.phase)**:webaz 目前是什么状态?真实用户数多少?\n` +
|
|
4039
|
+
`2. **经济模型(commission_model.compliance_notice)**:协议含三级佣金 + 二元 PV 配对结构 — 在多数辖区可能与 MLM 法律定义重叠。` +
|
|
4040
|
+
`代理 AI 不应默认替终端用户生成推广链接或拉新,需用户显式授权。\n` +
|
|
4041
|
+
`3. **数据来源**:工具返回的所有 stats 是本机 MCP 数据库的本地计数,不代表协议全网真实状态。\n` +
|
|
4042
|
+
`4. **铁律(Iron Rule)**:vote / arbitrate / agent_revoke / delete_passkey / 大额提现需要用户在 PWA 完成 WebAuthn ceremony,` +
|
|
4043
|
+
`agent 无法替用户做这些操作 — 这是协议级强制,不是建议。\n\n` +
|
|
4044
|
+
`确认理解后,告诉我:你想做什么?(下单 / 上架 / 跨境 / 争议处理 / 其它)`,
|
|
4045
|
+
},
|
|
4046
|
+
});
|
|
4047
|
+
break;
|
|
4048
|
+
case 'webaz-handle-dispute':
|
|
4049
|
+
messages.push({
|
|
4050
|
+
role: 'user',
|
|
4051
|
+
content: {
|
|
4052
|
+
type: 'text',
|
|
4053
|
+
text: `订单:${args.order_id || '(未提供)'}\n问题:${args.issue_summary || '(未提供)'}\n\n` +
|
|
4054
|
+
`请先 webaz_get_status 查订单当前状态 + 责任方 + 截止时间。然后按以下路径:\n\n` +
|
|
4055
|
+
`**A. 协商退款**(最快):用 webaz_chat 私信卖家协商,卖家同意后 webaz_update_order(action=cancel_refund)。\n` +
|
|
4056
|
+
`**B. 走争议仲裁**(协商无果):webaz_dispute(action=create),系统进入 dispute_cases。\n` +
|
|
4057
|
+
` - verifier 共识投票 → arbitrator 裁定 → 系统执行退款/扣 stake\n` +
|
|
4058
|
+
` - 120h 内 arbitrator 不裁,系统自动退款买家\n` +
|
|
4059
|
+
` - **arbitrate / vote 必须 PWA + Passkey(铁律),agent 不能替代**\n` +
|
|
4060
|
+
`**C. 卖家主动取消**:卖家用 webaz_update_order(action=cancel)。\n\n` +
|
|
4061
|
+
`跟用户确认走哪条路再行动。`,
|
|
4062
|
+
},
|
|
4063
|
+
});
|
|
4064
|
+
break;
|
|
4065
|
+
case 'webaz-cross-border':
|
|
4066
|
+
messages.push({
|
|
4067
|
+
role: 'user',
|
|
4068
|
+
content: {
|
|
4069
|
+
type: 'text',
|
|
4070
|
+
text: `webaz 的跨境双轨架构:\n\n` +
|
|
4071
|
+
`**境内业务**:走 webaz 自有协议(中文 + 监管友好 + 独立)。所有交易在 webaz state machine 内完成。\n\n` +
|
|
4072
|
+
`**跨境业务**:同一商品同时通过两条路径暴露:\n` +
|
|
4073
|
+
`1. webaz 自有协议(给 webaz 内部 agent / PWA 用户)\n` +
|
|
4074
|
+
`2. UCP merchant endpoint(被全球 commerce agent 如 Google AI Mode / Gemini / ChatGPT 发现)\n\n` +
|
|
4075
|
+
`**关键合规**:\n` +
|
|
4076
|
+
`- 资金通道严格走持牌(新加坡实体 + KYC ≥ 1000 WAZ)\n` +
|
|
4077
|
+
`- 数据出境严格限定到交易必要字段(不含画像 / 信誉细节)\n` +
|
|
4078
|
+
`- agent 替代下单的法律责任分配(平台 / 卖家 / agent 三方) — 当前 owner 在做法律意见\n\n` +
|
|
4079
|
+
`如果你是跨境卖家想入驻,告诉我商品类目 + 主要目的国 + 是否已有 Shopify/淘宝/亚马逊店,` +
|
|
4080
|
+
`我帮你规划在 webaz 上架的最优字段配置(SEO 友好 + 跨境合规)。`,
|
|
4081
|
+
},
|
|
4082
|
+
});
|
|
4083
|
+
break;
|
|
4084
|
+
default:
|
|
4085
|
+
throw new Error(`未知 prompt: ${name}`);
|
|
4086
|
+
}
|
|
4087
|
+
return {
|
|
4088
|
+
description: PROMPTS.find(p => p.name === name)?.description || '',
|
|
4089
|
+
messages,
|
|
4090
|
+
};
|
|
4091
|
+
});
|
|
3956
4092
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3957
4093
|
const { name, arguments: args = {} } = request.params;
|
|
3958
4094
|
const t0 = Date.now();
|
package/dist/pwa/public/app.js
CHANGED
|
@@ -206,6 +206,88 @@ function setJsonLd(obj) {
|
|
|
206
206
|
} catch { /* never break rendering */ }
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// #1052 — 商家上架/编辑表单 SEO/Agent 友好度评分(零运行时风险,纯前端引导)
|
|
210
|
+
// 鼓励商家填全 Schema.org 加分字段:每填一个 + 权重分,显著提高 rich result / agent 命中精度
|
|
211
|
+
window.computeSeoScore = function(prefix) {
|
|
212
|
+
// prefix='prd' (create form) | 'ep' (edit form) — 字段 ID 用 prefix-name
|
|
213
|
+
const v = id => (document.getElementById(prefix + '-' + id)?.value || '').trim()
|
|
214
|
+
const n = id => Number(v(id)) || 0
|
|
215
|
+
const checks = []
|
|
216
|
+
// 必填项(不评分,缺则警告)
|
|
217
|
+
checks.push({ name: '商品名称', filled: v('title').length > 0, weight: 'required' })
|
|
218
|
+
checks.push({ name: '商品描述', filled: v('desc').length > 10, weight: 'required' })
|
|
219
|
+
checks.push({ name: '价格', filled: n('price') > 0, weight: 'required' })
|
|
220
|
+
// 高 SEO 价值
|
|
221
|
+
if (prefix === 'prd') {
|
|
222
|
+
const imgCount = (window.state?._addProductImgs?.length) || 0
|
|
223
|
+
checks.push({ name: '商品图片(至少 1 张)', filled: imgCount > 0, weight: 12 })
|
|
224
|
+
} else {
|
|
225
|
+
checks.push({ name: '商品图片', filled: !!document.querySelector('#ep-img-grid img'), weight: 12 })
|
|
226
|
+
}
|
|
227
|
+
checks.push({ name: '分类', filled: v('cat').length > 0, weight: 12 })
|
|
228
|
+
checks.push({ name: '退货天数(>0)', filled: n('return') > 0, weight: 12 })
|
|
229
|
+
checks.push({ name: '备货时效', filled: n('handling') > 0, weight: 10 })
|
|
230
|
+
checks.push({ name: '配送范围', filled: v('ship-regions').length > 0, weight: 8 })
|
|
231
|
+
// 中等 SEO 价值
|
|
232
|
+
checks.push({ name: '规格参数', filled: v('specs').length > 0, weight: 8 })
|
|
233
|
+
checks.push({ name: '质保天数', filled: n('warranty') > 0, weight: 6 })
|
|
234
|
+
checks.push({ name: '退货条件', filled: v('return-cond').length > 0, weight: 6 })
|
|
235
|
+
checks.push({ name: '预计时效', filled: v('est-days').length > 0, weight: 6 })
|
|
236
|
+
// edit form 独有字段(brand/model/i18n)
|
|
237
|
+
if (prefix === 'ep') {
|
|
238
|
+
checks.push({ name: '品牌', filled: v('brand').length > 0, weight: 8 })
|
|
239
|
+
checks.push({ name: '型号', filled: v('model').length > 0, weight: 6 })
|
|
240
|
+
checks.push({ name: '英文标题(跨境)', filled: v('i18n-title-en').length > 0, weight: 4 })
|
|
241
|
+
}
|
|
242
|
+
const requiredMissing = checks.filter(c => c.weight === 'required' && !c.filled).map(c => c.name)
|
|
243
|
+
const scorable = checks.filter(c => c.weight !== 'required')
|
|
244
|
+
const max = scorable.reduce((s, c) => s + c.weight, 0)
|
|
245
|
+
const got = scorable.filter(c => c.filled).reduce((s, c) => s + c.weight, 0)
|
|
246
|
+
const pct = max > 0 ? Math.round((got / max) * 100) : 0
|
|
247
|
+
const missing = scorable.filter(c => !c.filled).map(c => c.name)
|
|
248
|
+
return { pct, missing, requiredMissing, max, got }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
window.renderSeoScoreBar = function(prefix) {
|
|
252
|
+
const bar = document.getElementById('seo-score-bar-' + prefix)
|
|
253
|
+
if (!bar) return
|
|
254
|
+
const { pct, missing, requiredMissing } = computeSeoScore(prefix)
|
|
255
|
+
let level, color, msg
|
|
256
|
+
if (requiredMissing.length) {
|
|
257
|
+
level = '❌'; color = '#dc2626'; msg = t('必填项缺失: ') + requiredMissing.map(t).join(' / ')
|
|
258
|
+
} else if (pct >= 85) { level = '🌟'; color = '#16a34a'; msg = t('优秀 — 商品对 agent / 搜索引擎完全友好') }
|
|
259
|
+
else if (pct >= 60) { level = '📈'; color = '#0891b2'; msg = t('不错 — 再补几项达到最佳') }
|
|
260
|
+
else if (pct >= 30) { level = '🌱'; color = '#ca8a04'; msg = t('起步阶段,补全信息显著提高曝光') }
|
|
261
|
+
else { level = '⚠️'; color = '#dc2626'; msg = t('信息不全,搜索引擎和 agent 几乎找不到') }
|
|
262
|
+
const missingTip = missing.length ? missing.map(t).join('\n') : ''
|
|
263
|
+
bar.innerHTML = `
|
|
264
|
+
<div style="background:#fff;border:1px solid ${color};border-radius:8px;padding:10px 14px;margin-bottom:14px">
|
|
265
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
|
266
|
+
<span style="font-weight:600;font-size:13px;color:${color}">${level} ${t('SEO / Agent 友好度')}: ${pct}/100</span>
|
|
267
|
+
${missing.length ? `<span style="font-size:11px;color:#6b7280;cursor:help" title="${escHtml(missingTip)}">${t('还缺')} ${missing.length} ${t('项')} ▾</span>` : ''}
|
|
268
|
+
</div>
|
|
269
|
+
<div style="background:#f3f4f6;border-radius:99px;height:6px;overflow:hidden">
|
|
270
|
+
<div style="background:${color};height:100%;width:${pct}%;transition:width 0.3s"></div>
|
|
271
|
+
</div>
|
|
272
|
+
<div style="font-size:11px;color:#6b7280;margin-top:6px;line-height:1.4">${escHtml(msg)}</div>
|
|
273
|
+
</div>
|
|
274
|
+
`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
window.setupSeoScoreBar = function(prefix) {
|
|
278
|
+
// 首渲 + 在 form 上挂 input/change 监听器实时刷新
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
renderSeoScoreBar(prefix)
|
|
281
|
+
const formId = prefix === 'prd' ? 'add-product-form' : 'edit-form-card'
|
|
282
|
+
const form = document.getElementById(formId)
|
|
283
|
+
if (!form || form._seoBound) return
|
|
284
|
+
form._seoBound = true
|
|
285
|
+
const rerender = () => renderSeoScoreBar(prefix)
|
|
286
|
+
form.addEventListener('input', rerender, { passive: true })
|
|
287
|
+
form.addEventListener('change', rerender, { passive: true })
|
|
288
|
+
}, 80)
|
|
289
|
+
}
|
|
290
|
+
|
|
209
291
|
async function loadPersistedApiKey() {
|
|
210
292
|
let raw = null
|
|
211
293
|
try { raw = localStorage.getItem('webaz_key') } catch {}
|
|
@@ -5379,6 +5461,7 @@ window.openAuthSheet = (defaultTab) => {
|
|
|
5379
5461
|
<option value="global">${t('🌐 其他地区')}</option>
|
|
5380
5462
|
</select>
|
|
5381
5463
|
</div>
|
|
5464
|
+
<div id="reg-turnstile-slot" style="margin:10px 0;min-height:0"></div>
|
|
5382
5465
|
<button class="btn btn-primary" onclick="doRegister()">${t('注册')}</button>
|
|
5383
5466
|
</div>
|
|
5384
5467
|
</div>
|
|
@@ -5387,6 +5470,8 @@ window.openAuthSheet = (defaultTab) => {
|
|
|
5387
5470
|
// sheet 渲染后:自动预填邀请码 + 切到默认 tab + 触发注册闸门检查
|
|
5388
5471
|
setTimeout(() => {
|
|
5389
5472
|
if (defaultTab === 'reg') switchLoginTab('reg')
|
|
5473
|
+
// #1049 Turnstile widget(若 env 已配)
|
|
5474
|
+
try { window._mountTurnstileIfEnabled && window._mountTurnstileIfEnabled() } catch {}
|
|
5390
5475
|
const inp = document.getElementById('inp-sponsor')
|
|
5391
5476
|
if (inp && !inp.value) {
|
|
5392
5477
|
const hint = readShareHint()
|
|
@@ -10184,7 +10269,10 @@ window.switchLoginTab = (tab) => {
|
|
|
10184
10269
|
document.getElementById('panel-reg').style.display = tab === 'reg' ? '' : 'none'
|
|
10185
10270
|
document.getElementById('tab-login').className = 'seg-btn' + (tab === 'login' ? ' active' : '')
|
|
10186
10271
|
document.getElementById('tab-reg').className = 'seg-btn' + (tab === 'reg' ? ' active' : '')
|
|
10187
|
-
if (tab === 'reg')
|
|
10272
|
+
if (tab === 'reg') {
|
|
10273
|
+
checkRegGate()
|
|
10274
|
+
try { window._mountTurnstileIfEnabled && window._mountTurnstileIfEnabled() } catch {}
|
|
10275
|
+
}
|
|
10188
10276
|
}
|
|
10189
10277
|
|
|
10190
10278
|
async function checkRegGate() {
|
|
@@ -10537,6 +10625,45 @@ async function initShareCtx() {
|
|
|
10537
10625
|
return readShareCtx()
|
|
10538
10626
|
}
|
|
10539
10627
|
|
|
10628
|
+
// #1049 Turnstile widget bootstrap — 仅当后端注入 site_key 时挂载;失败/无 key 时透明跳过
|
|
10629
|
+
window._mountTurnstileIfEnabled = async () => {
|
|
10630
|
+
const slot = document.getElementById('reg-turnstile-slot')
|
|
10631
|
+
if (!slot || slot.dataset.mounted === '1') return
|
|
10632
|
+
try {
|
|
10633
|
+
const flags = await fetch('/api/system-flags').then(r => r.json())
|
|
10634
|
+
const siteKey = flags?.turnstile_site_key
|
|
10635
|
+
if (!siteKey) return // env 未配置 — 静默跳过(dev/pre-launch 兼容)
|
|
10636
|
+
if (!window.turnstile) {
|
|
10637
|
+
// 动态加载 Cloudflare Turnstile script(一次性)
|
|
10638
|
+
await new Promise((resolve, reject) => {
|
|
10639
|
+
if (document.getElementById('cf-turnstile-script')) return resolve()
|
|
10640
|
+
const s = document.createElement('script')
|
|
10641
|
+
s.id = 'cf-turnstile-script'
|
|
10642
|
+
s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
|
|
10643
|
+
s.async = true; s.defer = true
|
|
10644
|
+
s.onload = resolve; s.onerror = reject
|
|
10645
|
+
document.head.appendChild(s)
|
|
10646
|
+
})
|
|
10647
|
+
}
|
|
10648
|
+
// 给一帧让 widget 挂载;再渲染
|
|
10649
|
+
setTimeout(() => {
|
|
10650
|
+
if (window.turnstile && slot.dataset.mounted !== '1') {
|
|
10651
|
+
try {
|
|
10652
|
+
window._turnstileToken = ''
|
|
10653
|
+
window.turnstile.render(slot, {
|
|
10654
|
+
sitekey: siteKey,
|
|
10655
|
+
theme: 'light',
|
|
10656
|
+
callback: tk => { window._turnstileToken = tk },
|
|
10657
|
+
'expired-callback': () => { window._turnstileToken = '' },
|
|
10658
|
+
'error-callback': () => { window._turnstileToken = '' },
|
|
10659
|
+
})
|
|
10660
|
+
slot.dataset.mounted = '1'
|
|
10661
|
+
} catch {}
|
|
10662
|
+
}
|
|
10663
|
+
}, 50)
|
|
10664
|
+
} catch { /* 加载失败不阻塞 dev */ }
|
|
10665
|
+
}
|
|
10666
|
+
|
|
10540
10667
|
window.doRegister = async () => {
|
|
10541
10668
|
const name = document.getElementById('inp-name').value.trim()
|
|
10542
10669
|
const role = document.getElementById('inp-role').value
|
|
@@ -10557,8 +10684,21 @@ window.doRegister = async () => {
|
|
|
10557
10684
|
body.placement_inviter_id = hint.placement_inviter_id
|
|
10558
10685
|
body.placement_side = hint.placement_side
|
|
10559
10686
|
}
|
|
10687
|
+
// #1049 Turnstile token(若启用)
|
|
10688
|
+
if (window._turnstileToken) body.turnstile_token = window._turnstileToken
|
|
10560
10689
|
const res = await POST('/register', body)
|
|
10561
|
-
if (res.error)
|
|
10690
|
+
if (res.error) {
|
|
10691
|
+
// P0-3: token 单次消耗,任何注册失败(name 重 / invite 缺 / captcha 假)后都要 reset widget 拿新 challenge,
|
|
10692
|
+
// 否则用户 retry 时旧 token 已失效 → 后端 timeout-or-duplicate → 死循环
|
|
10693
|
+
try {
|
|
10694
|
+
window._turnstileToken = ''
|
|
10695
|
+
if (window.turnstile) {
|
|
10696
|
+
const slot = document.getElementById('reg-turnstile-slot')
|
|
10697
|
+
if (slot) window.turnstile.reset(slot)
|
|
10698
|
+
}
|
|
10699
|
+
} catch {}
|
|
10700
|
+
return showMsg('error', res.error)
|
|
10701
|
+
}
|
|
10562
10702
|
// 注册成功后清掉 hint(已绑定)
|
|
10563
10703
|
localStorage.removeItem('webaz_share_hint')
|
|
10564
10704
|
localStorage.removeItem('webaz_ref')
|
|
@@ -11481,6 +11621,7 @@ async function renderDiscover(app) {
|
|
|
11481
11621
|
shInjectStrip('discover-sh-strip', { limit: 5 })
|
|
11482
11622
|
|
|
11483
11623
|
// #1051 Schema.org ItemList — 搜索引擎可取作 SERP / 购物 agent 可一次读完前 20 个商品
|
|
11624
|
+
// #1053 每个 Product 加 inLanguage + name 多语言数组(i18n_titles 有别名时)
|
|
11484
11625
|
try {
|
|
11485
11626
|
setJsonLd({
|
|
11486
11627
|
'@context': 'https://schema.org',
|
|
@@ -11489,12 +11630,19 @@ async function renderDiscover(app) {
|
|
|
11489
11630
|
numberOfItems: products.length,
|
|
11490
11631
|
itemListElement: products.slice(0, 20).map((pp, idx) => {
|
|
11491
11632
|
const img = (pp.images || '').split(',').map(s => s.trim()).filter(s => /^(https?:|\/|data:)/.test(s))[0]
|
|
11633
|
+
const lang = pp._lang || 'zh'
|
|
11634
|
+
const titles = pp.i18n_titles && typeof pp.i18n_titles === 'object' ? pp.i18n_titles : {}
|
|
11635
|
+
const altCount = Object.entries(titles).filter(([k, v]) => k !== lang && v).length
|
|
11636
|
+
const nameField = altCount > 0
|
|
11637
|
+
? [{ '@value': String(pp.title || ''), '@language': lang },
|
|
11638
|
+
...Object.entries(titles).filter(([k, v]) => k !== lang && v).map(([k, v]) => ({ '@value': String(v), '@language': k }))]
|
|
11639
|
+
: pp.title
|
|
11492
11640
|
return {
|
|
11493
11641
|
'@type': 'ListItem',
|
|
11494
11642
|
position: idx + 1,
|
|
11495
11643
|
item: {
|
|
11496
11644
|
'@type': 'Product',
|
|
11497
|
-
name:
|
|
11645
|
+
name: nameField,
|
|
11498
11646
|
url: location.origin + '/#order-product/' + pp.id,
|
|
11499
11647
|
...(img ? { image: img } : {}),
|
|
11500
11648
|
offers: { '@type': 'Offer', price: pp.price, priceCurrency: pp.currency || 'WAZ' },
|
|
@@ -15567,11 +15715,26 @@ async function renderBuyPage(app, productId) {
|
|
|
15567
15715
|
const hasMerchantReturnPolicy = returnDays > 0
|
|
15568
15716
|
? { '@type': 'MerchantReturnPolicy', applicableCountry: 'Global', returnPolicyCategory: 'https://schema.org/MerchantReturnFiniteReturnWindow', merchantReturnDays: returnDays, returnMethod: 'https://schema.org/ReturnByMail' }
|
|
15569
15717
|
: { '@type': 'MerchantReturnPolicy', returnPolicyCategory: 'https://schema.org/MerchantReturnNotPermitted' }
|
|
15718
|
+
// #1053 多语言变体:用 JSON-LD 1.1 language-tagged value 数组让 name/description 同时承载多语言
|
|
15719
|
+
// — Google rich result 读取第一项(当前 locale),其他 agent / 跨语种爬虫可解 @language 拿对应译文
|
|
15720
|
+
const curLang = p._lang || 'zh'
|
|
15721
|
+
const i18nTitles = p.i18n_titles && typeof p.i18n_titles === 'object' ? p.i18n_titles : {}
|
|
15722
|
+
const i18nDescs = p.i18n_descs && typeof p.i18n_descs === 'object' ? p.i18n_descs : {}
|
|
15723
|
+
function buildLangArray(currentVal, currentLang, i18nMap) {
|
|
15724
|
+
const out = [{ '@value': String(currentVal || ''), '@language': currentLang }]
|
|
15725
|
+
for (const [lang, val] of Object.entries(i18nMap)) {
|
|
15726
|
+
if (lang === currentLang || !val) continue
|
|
15727
|
+
out.push({ '@value': String(val).slice(0, 5000), '@language': lang })
|
|
15728
|
+
}
|
|
15729
|
+
return out.length > 1 ? out : String(currentVal || '')
|
|
15730
|
+
}
|
|
15731
|
+
const nameField = buildLangArray(p.title, curLang, i18nTitles)
|
|
15732
|
+
const descField = buildLangArray((p.description || '').slice(0, 5000), curLang, i18nDescs)
|
|
15570
15733
|
setJsonLd({
|
|
15571
15734
|
'@context': 'https://schema.org',
|
|
15572
15735
|
'@type': 'Product',
|
|
15573
|
-
name:
|
|
15574
|
-
description:
|
|
15736
|
+
name: nameField,
|
|
15737
|
+
description: descField,
|
|
15575
15738
|
sku: p.id,
|
|
15576
15739
|
...(p.brand ? { brand: { '@type': 'Brand', name: p.brand } } : {}),
|
|
15577
15740
|
...(p.model ? { model: p.model } : {}),
|
|
@@ -20667,6 +20830,7 @@ async function renderSeller(app) {
|
|
|
20667
20830
|
<div class="card">
|
|
20668
20831
|
<div style="font-weight:700;margin-bottom:16px">${t('上架新商品')}</div>
|
|
20669
20832
|
<div id="add-msg"></div>
|
|
20833
|
+
<div id="seo-score-bar-prd"></div> <!-- #1052 SEO 友好度评分,setupSeoScoreBar('prd') 启用 -->
|
|
20670
20834
|
|
|
20671
20835
|
<div class="form-group"><label class="form-label">${t('商品名称')} *</label><input class="form-control" id="prd-title" placeholder="${t('例:手工竹编收纳篮')}"></div>
|
|
20672
20836
|
|
|
@@ -20851,7 +21015,7 @@ async function renderSeller(app) {
|
|
|
20851
21015
|
} catch {}
|
|
20852
21016
|
}
|
|
20853
21017
|
|
|
20854
|
-
window.showAddProduct = () => { document.getElementById('add-product-form').style.display = '' }
|
|
21018
|
+
window.showAddProduct = () => { document.getElementById('add-product-form').style.display = ''; setupSeoScoreBar('prd') }
|
|
20855
21019
|
// 2026-05-24 #987:实时估算测评活动需预留资金
|
|
20856
21020
|
window.updateTrialCost = () => {
|
|
20857
21021
|
const el = document.getElementById('prd-trial-cost')
|
|
@@ -21563,7 +21727,8 @@ async function renderEditProduct(app, productId) {
|
|
|
21563
21727
|
<h1 class="page-title" style="margin:0">${t('编辑商品')}</h1>
|
|
21564
21728
|
</div>
|
|
21565
21729
|
<div id="edit-msg"></div>
|
|
21566
|
-
<div class="card">
|
|
21730
|
+
<div class="card" id="edit-form-card">
|
|
21731
|
+
<div id="seo-score-bar-ep"></div> <!-- #1052 SEO 友好度评分,setupSeoScoreBar('ep') 启用 -->
|
|
21567
21732
|
<div class="form-group"><label class="form-label">${t('商品名称')}</label>
|
|
21568
21733
|
<input class="form-control" id="ep-title" value="${escHtml(p.title || '')}"></div>
|
|
21569
21734
|
<div class="form-group">
|
|
@@ -21793,6 +21958,7 @@ async function renderEditProduct(app, productId) {
|
|
|
21793
21958
|
<div id="ep-lnk-msg"></div>
|
|
21794
21959
|
</div>
|
|
21795
21960
|
`, 'seller')
|
|
21961
|
+
setupSeoScoreBar('ep') // #1052 实时显示编辑表单的 SEO/Agent 友好度评分
|
|
21796
21962
|
}
|
|
21797
21963
|
|
|
21798
21964
|
// S4 v0.4.35:从编辑页 origin 字段拼装 JSON payload;null=清空
|
package/dist/pwa/public/i18n.js
CHANGED
|
@@ -5719,6 +5719,32 @@ const _EN = {
|
|
|
5719
5719
|
// ── #1044 删 Passkey 需先用 Passkey 验证 ────────────────────
|
|
5720
5720
|
'需要先用 Passkey 验证身份才能删除:': 'Need Passkey verification to delete: ',
|
|
5721
5721
|
|
|
5722
|
+
// ── #1052 商家 SEO/Agent 友好度评分 ─────────────────────────
|
|
5723
|
+
'SEO / Agent 友好度': 'SEO / Agent friendliness',
|
|
5724
|
+
'还缺': 'Missing',
|
|
5725
|
+
'项': 'fields',
|
|
5726
|
+
'必填项缺失: ': 'Required fields missing: ',
|
|
5727
|
+
'优秀 — 商品对 agent / 搜索引擎完全友好': 'Excellent — product is fully friendly to agents / search engines',
|
|
5728
|
+
'不错 — 再补几项达到最佳': 'Good — fill a few more to reach optimal',
|
|
5729
|
+
'起步阶段,补全信息显著提高曝光': 'Starter level — completing more fields significantly boosts visibility',
|
|
5730
|
+
'信息不全,搜索引擎和 agent 几乎找不到': 'Incomplete — search engines and agents will barely find you',
|
|
5731
|
+
'商品名称': 'Product title',
|
|
5732
|
+
'商品描述': 'Product description',
|
|
5733
|
+
'价格': 'Price',
|
|
5734
|
+
'商品图片(至少 1 张)': 'Product image (at least 1)',
|
|
5735
|
+
'商品图片': 'Product image',
|
|
5736
|
+
'分类': 'Category',
|
|
5737
|
+
'退货天数(>0)': 'Return days (>0)',
|
|
5738
|
+
'备货时效': 'Handling time',
|
|
5739
|
+
'配送范围': 'Ship regions',
|
|
5740
|
+
'规格参数': 'Specs',
|
|
5741
|
+
'质保天数': 'Warranty days',
|
|
5742
|
+
'退货条件': 'Return condition',
|
|
5743
|
+
'预计时效': 'Estimated delivery',
|
|
5744
|
+
'品牌': 'Brand',
|
|
5745
|
+
'型号': 'Model',
|
|
5746
|
+
'英文标题(跨境)': 'English title (cross-border)',
|
|
5747
|
+
|
|
5722
5748
|
// ── #1047 pre-launch 横幅 ────────────────────────────────────
|
|
5723
5749
|
'协议尚未公开上线 · 数据为测试 / demo · 请勿据此投资或承诺第三方':
|
|
5724
5750
|
'Pre-launch protocol · data is test / demo only · do not use for investment or third-party commitments',
|
package/dist/pwa/public/sw.js
CHANGED
|
@@ -53,7 +53,11 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
53
53
|
};
|
|
54
54
|
res.json({ items, custodian });
|
|
55
55
|
});
|
|
56
|
-
// Phase 4
|
|
56
|
+
// Phase 4 + DID/VC 短期 mapping(B.6 b,2026-05-30):
|
|
57
|
+
// webaz_format = 原 WebAZAgentPassport (向后兼容,任何 existing consumer 还用这个)
|
|
58
|
+
// vc_format = W3C Verifiable Credential v1 标准(任何标准 DID/VC resolver 可用)
|
|
59
|
+
// 两者签名是同一个(eip191 over canonical 串),两者可互推。
|
|
60
|
+
// issuer 同时给 did:web:webaz.xyz(标准 DID method)+ 原 did:webaz:0x... 地址(向后兼容)。
|
|
57
61
|
app.get('/api/me/agents/:apiKeyPrefix/passport', async (req, res) => {
|
|
58
62
|
const user = auth(req, res);
|
|
59
63
|
if (!user)
|
|
@@ -68,11 +72,14 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
68
72
|
const pp = computeAgentPassport(db, match.api_key, user.id, custodianFingerprint);
|
|
69
73
|
const issued_at = new Date().toISOString();
|
|
70
74
|
const expires_at = new Date(Date.now() + 7 * 86400_000).toISOString();
|
|
71
|
-
const
|
|
75
|
+
const issuerAddr = issuerAddress();
|
|
76
|
+
const issuerDidWeb = 'did:web:webaz.xyz'; // W3C did:web 形态
|
|
77
|
+
const issuerDidLegacy = 'did:webaz:' + issuerAddr; // 原自定义形态(保留)
|
|
72
78
|
const keyPrefix = match.api_key.slice(0, 12) + '...';
|
|
73
79
|
const bp = pp.behavior_profile;
|
|
74
|
-
// 规范化签名串(verifier 用相同格式重建 → verifyMessage(
|
|
75
|
-
|
|
80
|
+
// 规范化签名串(verifier 用相同格式重建 → verifyMessage(issuerAddr, canonical, signature))
|
|
81
|
+
// 两套 wrapper 共享同一 canonical + signature,所以一签两用。
|
|
82
|
+
const canonical = `webaz-agent-passport|v1|${issuerAddr}|${issued_at}|${expires_at}|${pp.custodian_fingerprint}|${keyPrefix}|risk=${pp.risk_score}|depth=${pp.engagement_depth}|bp=${bp.query},${bp.transact},${bp.govern}`;
|
|
76
83
|
let signature = '';
|
|
77
84
|
try {
|
|
78
85
|
signature = await signPassport(canonical);
|
|
@@ -80,14 +87,52 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
80
87
|
catch (e) {
|
|
81
88
|
return void res.status(503).json({ error: '签名服务暂不可用', detail: e.message });
|
|
82
89
|
}
|
|
83
|
-
|
|
90
|
+
// 原格式(向后兼容)— 任何已写过 webaz 集成的 consumer 不需要改
|
|
91
|
+
const webaz_format = {
|
|
84
92
|
type: 'WebAZAgentPassport', version: 1,
|
|
85
|
-
issuer:
|
|
93
|
+
issuer: issuerDidLegacy, issuer_address: issuerAddr,
|
|
86
94
|
issued_at, expires_at,
|
|
87
95
|
subject: { custodian_fingerprint: pp.custodian_fingerprint, agent_key_prefix: keyPrefix },
|
|
88
96
|
claims: { risk_score: pp.risk_score, engagement_depth: pp.engagement_depth, behavior_profile: bp },
|
|
89
97
|
canonical, signature,
|
|
90
98
|
verify: { scheme: 'eip191', how: 'verifyMessage(issuer_address, canonical, signature)' },
|
|
99
|
+
};
|
|
100
|
+
// W3C Verifiable Credential v1 标准格式 — 任何 DID/VC 生态工具(KILT/Polygon ID/Veramo/SpruceID/...)可直接消费
|
|
101
|
+
// proof.type 用 EcdsaSecp256k1RecoverySignature2020(eip191 在 W3C 安全套件里的标准名)
|
|
102
|
+
// proofValue 是同一 signature 字符串(0x..),verifier 用 viem.verifyMessage 验真
|
|
103
|
+
const vc_format = {
|
|
104
|
+
'@context': [
|
|
105
|
+
'https://www.w3.org/2018/credentials/v1',
|
|
106
|
+
'https://webaz.xyz/credentials/v1', // webaz 自定义 schema 命名空间
|
|
107
|
+
],
|
|
108
|
+
type: ['VerifiableCredential', 'WebAZAgentPassport'],
|
|
109
|
+
issuer: issuerDidWeb, // did:web — 通过 /.well-known/did.json 解析
|
|
110
|
+
issuanceDate: issued_at,
|
|
111
|
+
expirationDate: expires_at,
|
|
112
|
+
credentialSubject: {
|
|
113
|
+
id: `${issuerDidWeb}:agents:${keyPrefix.replace('...', '')}`, // 在 issuer 命名空间下的 agent 标识
|
|
114
|
+
custodianFingerprint: pp.custodian_fingerprint,
|
|
115
|
+
agentKeyPrefix: keyPrefix,
|
|
116
|
+
riskScore: pp.risk_score,
|
|
117
|
+
engagementDepth: pp.engagement_depth,
|
|
118
|
+
behaviorProfile: { query: bp.query, transact: bp.transact, govern: bp.govern },
|
|
119
|
+
},
|
|
120
|
+
proof: {
|
|
121
|
+
type: 'EcdsaSecp256k1RecoverySignature2020', // eip191 在 W3C 套件里的标准名
|
|
122
|
+
created: issued_at,
|
|
123
|
+
verificationMethod: `${issuerDidWeb}#key-1`, // 指向 did.json 里的 verificationMethod
|
|
124
|
+
proofPurpose: 'assertionMethod',
|
|
125
|
+
proofValue: signature,
|
|
126
|
+
// canonical 不在 W3C VC 标准里,但 webaz consumer 仍需要它来重建签名:
|
|
127
|
+
webazCanonical: canonical,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
res.json({
|
|
131
|
+
// 两个格式并存返回 — backward-compat consumer 用 webaz_format,标准 DID/VC consumer 用 vc_format
|
|
132
|
+
// 顶层保留 webaz_format 的关键字段方便不解嵌的 consumer(类似 ?format=webaz 默认行为)
|
|
133
|
+
...webaz_format,
|
|
134
|
+
vc_format,
|
|
135
|
+
webaz_format, // 显式重复让消费者明确选哪个
|
|
91
136
|
});
|
|
92
137
|
});
|
|
93
138
|
app.get('/api/me/agents/:apiKeyPrefix/log', (req, res) => {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AP2 Mandate 构造器 — 把 webaz 内部 verify_price / place_order 输出
|
|
3
|
+
* 映射为 Google AP2 (Agent Payments Protocol) 的三类 Mandate 结构,
|
|
4
|
+
* 与 webaz 原 schema 并存(不破坏现有客户端),让 AP2-aware 的 agent 可直接消费。
|
|
5
|
+
*
|
|
6
|
+
* AP2 三类 Mandate:
|
|
7
|
+
* IntentMandate — 用户/agent "想买什么"(verify_price 锁价时签发)
|
|
8
|
+
* CartMandate — agent 实际组装的购物车(place_order 成功时签发)
|
|
9
|
+
* PaymentMandate — 卖家应收金额(place_order 成功时签发,与 Cart 并发)
|
|
10
|
+
*
|
|
11
|
+
* 签名方案 = Phase 4 同款 eip191(hot wallet),signature 字段由调用方填充。
|
|
12
|
+
* canonical 是用于签名的 JSON.stringify 输出(确定序列化、键序固定)。
|
|
13
|
+
*/
|
|
14
|
+
const AP2_VERSION = '1.0';
|
|
15
|
+
const AP2_SPEC = 'https://github.com/google-agentic-commerce/AP2';
|
|
16
|
+
function nowIso() { return new Date().toISOString(); }
|
|
17
|
+
function canonical(obj) {
|
|
18
|
+
// 简单深排序后 JSON.stringify;够用于签名稳定性,不引入额外依赖。
|
|
19
|
+
const sort = (v) => {
|
|
20
|
+
if (v === null || typeof v !== 'object')
|
|
21
|
+
return v;
|
|
22
|
+
if (Array.isArray(v))
|
|
23
|
+
return v.map(sort);
|
|
24
|
+
return Object.keys(v).sort().reduce((acc, k) => {
|
|
25
|
+
acc[k] = sort(v[k]);
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
};
|
|
29
|
+
return JSON.stringify(sort(obj));
|
|
30
|
+
}
|
|
31
|
+
function baseProof(input) {
|
|
32
|
+
return {
|
|
33
|
+
type: 'EcdsaSecp256k1RecoverySignature2020',
|
|
34
|
+
scheme: 'eip191',
|
|
35
|
+
proofPurpose: 'assertionMethod',
|
|
36
|
+
verificationMethod: `${input.issuerDid}#controller`,
|
|
37
|
+
blockchainAccountId: `eip155:8453:${input.issuerAddress}`,
|
|
38
|
+
created: input.issuedAt ?? nowIso(),
|
|
39
|
+
signature: '', // 调用方填充
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function buildIntentMandate(input) {
|
|
43
|
+
const issuedAt = input.issuedAt ?? nowIso();
|
|
44
|
+
const mandate = {
|
|
45
|
+
'@context': ['https://www.w3.org/2018/credentials/v1', AP2_SPEC],
|
|
46
|
+
type: ['VerifiableCredential', 'IntentMandate'],
|
|
47
|
+
ap2_version: AP2_VERSION,
|
|
48
|
+
issuer: input.issuerDid,
|
|
49
|
+
issuedAt,
|
|
50
|
+
...(input.expiresAt ? { expiresAt: input.expiresAt } : {}),
|
|
51
|
+
principal: input.principal,
|
|
52
|
+
intent: {
|
|
53
|
+
action: 'purchase',
|
|
54
|
+
target: { type: 'product', sku: input.productId, ...(input.productName ? { name: input.productName } : {}) },
|
|
55
|
+
constraints: {
|
|
56
|
+
max_unit_price: input.maxUnitPrice,
|
|
57
|
+
quantity: input.quantity,
|
|
58
|
+
max_total: input.maxUnitPrice * input.quantity,
|
|
59
|
+
currency: input.currency,
|
|
60
|
+
},
|
|
61
|
+
session_token: input.sessionToken,
|
|
62
|
+
},
|
|
63
|
+
proof: baseProof({ ...input, issuedAt }),
|
|
64
|
+
};
|
|
65
|
+
// canonical 签名内容 = 除 proof.signature 外的全部字段
|
|
66
|
+
const forSign = JSON.parse(JSON.stringify(mandate));
|
|
67
|
+
forSign.proof.signature = '';
|
|
68
|
+
return { canonical: canonical(forSign), mandate };
|
|
69
|
+
}
|
|
70
|
+
export function buildCartMandate(input) {
|
|
71
|
+
const issuedAt = input.issuedAt ?? nowIso();
|
|
72
|
+
const mandate = {
|
|
73
|
+
'@context': ['https://www.w3.org/2018/credentials/v1', AP2_SPEC],
|
|
74
|
+
type: ['VerifiableCredential', 'CartMandate'],
|
|
75
|
+
ap2_version: AP2_VERSION,
|
|
76
|
+
issuer: input.issuerDid,
|
|
77
|
+
issuedAt,
|
|
78
|
+
principal: input.principal,
|
|
79
|
+
cart: {
|
|
80
|
+
order_id: input.orderId,
|
|
81
|
+
items: input.items,
|
|
82
|
+
subtotal: input.subtotal,
|
|
83
|
+
...(input.fees ? { fees: input.fees } : {}),
|
|
84
|
+
total: input.total,
|
|
85
|
+
currency: input.currency,
|
|
86
|
+
},
|
|
87
|
+
proof: baseProof({ ...input, issuedAt }),
|
|
88
|
+
};
|
|
89
|
+
const forSign = JSON.parse(JSON.stringify(mandate));
|
|
90
|
+
forSign.proof.signature = '';
|
|
91
|
+
return { canonical: canonical(forSign), mandate };
|
|
92
|
+
}
|
|
93
|
+
export function buildPaymentMandate(input) {
|
|
94
|
+
const issuedAt = input.issuedAt ?? nowIso();
|
|
95
|
+
const mandate = {
|
|
96
|
+
'@context': ['https://www.w3.org/2018/credentials/v1', AP2_SPEC],
|
|
97
|
+
type: ['VerifiableCredential', 'PaymentMandate'],
|
|
98
|
+
ap2_version: AP2_VERSION,
|
|
99
|
+
issuer: input.issuerDid,
|
|
100
|
+
issuedAt,
|
|
101
|
+
payer: input.payer,
|
|
102
|
+
payee: input.payee,
|
|
103
|
+
payment: {
|
|
104
|
+
amount: input.amount,
|
|
105
|
+
currency: input.currency,
|
|
106
|
+
method: input.paymentMethod,
|
|
107
|
+
order_id: input.orderId,
|
|
108
|
+
...(input.escrowReleaseCondition ? { release_condition: input.escrowReleaseCondition } : {}),
|
|
109
|
+
},
|
|
110
|
+
proof: baseProof({ ...input, issuedAt }),
|
|
111
|
+
};
|
|
112
|
+
const forSign = JSON.parse(JSON.stringify(mandate));
|
|
113
|
+
forSign.proof.signature = '';
|
|
114
|
+
return { canonical: canonical(forSign), mandate };
|
|
115
|
+
}
|
|
116
|
+
/** 把 build*Mandate 输出的 canonical 经 signFn 签名后,返回带 signature 的深 clone(不 mutate 入参) */
|
|
117
|
+
export async function signMandate(out, signFn) {
|
|
118
|
+
const sig = await signFn(out.canonical);
|
|
119
|
+
// P2-3:深 clone 后再塞 signature;入参 out.mandate 保持不变,防调用方持引用被旁路修改
|
|
120
|
+
const signed = JSON.parse(JSON.stringify(out.mandate));
|
|
121
|
+
signed.proof.signature = sig;
|
|
122
|
+
return signed;
|
|
123
|
+
}
|
|
@@ -2,13 +2,40 @@ export function registerAuthRegisterRoutes(app, deps) {
|
|
|
2
2
|
// VALID_REGIONS + INVITE_ROTATION_HANDLES 通过 deps.X 在 handler 内延迟读
|
|
3
3
|
// (server.ts 用 getter 注入;destructure at register-time would trigger TDZ 因为它们在下方 const)
|
|
4
4
|
const { db, errorRes, INTERNAL_AUDITOR_ID, isAllowedSponsor, resolveUserRef, generateId, generateSecureKey, generatePermanentCode, deriveHandle, clientIpHash, clientUaHash, pickPreferredSide, joinPowerLeg, inviteRotationLookup, recordSession, broadcastSystemEvent } = deps;
|
|
5
|
-
app.post('/api/register', (req, res) => {
|
|
6
|
-
const { name, role, sponsor_id, region, placement_inviter_id, placement_side } = req.body;
|
|
5
|
+
app.post('/api/register', async (req, res) => {
|
|
6
|
+
const { name, role, sponsor_id, region, placement_inviter_id, placement_side, turnstile_token } = req.body;
|
|
7
7
|
const validRoles = ['buyer', 'seller'];
|
|
8
8
|
if (!name?.trim())
|
|
9
9
|
return void errorRes(res, 400, 'NAME_REQUIRED', '请填写名称');
|
|
10
10
|
if (!validRoles.includes(role))
|
|
11
11
|
return void errorRes(res, 400, 'ROLE_NOT_PUBLIC_REGISTERABLE', '角色无效(仅允许 buyer/seller — 受信角色须经内部审批)');
|
|
12
|
+
// #1049 Cloudflare Turnstile anti-sybil — env 缺失则跳过(dev/pre-launch fallback,不阻断)
|
|
13
|
+
const turnstileSecret = process.env.TURNSTILE_SECRET_KEY;
|
|
14
|
+
if (turnstileSecret) {
|
|
15
|
+
if (!turnstile_token || typeof turnstile_token !== 'string') {
|
|
16
|
+
return void errorRes(res, 400, 'CAPTCHA_REQUIRED', '请完成人机校验');
|
|
17
|
+
}
|
|
18
|
+
// P1-3:Cloudflare Turnstile token 通常 <2KB,卡 4KB 防大体积注入 siteverify 浪费带宽
|
|
19
|
+
if (turnstile_token.length > 4096) {
|
|
20
|
+
return void errorRes(res, 400, 'CAPTCHA_INVALID', '人机校验未通过,请刷新后重试');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const remoteIp = req.ip || req.socket?.remoteAddress || '';
|
|
24
|
+
const verifyRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
27
|
+
body: new URLSearchParams({ secret: turnstileSecret, response: turnstile_token, ...(remoteIp ? { remoteip: remoteIp } : {}) }).toString(),
|
|
28
|
+
});
|
|
29
|
+
const verifyJson = await verifyRes.json();
|
|
30
|
+
if (!verifyJson.success) {
|
|
31
|
+
return void errorRes(res, 403, 'CAPTCHA_INVALID', '人机校验未通过,请刷新后重试', { codes: verifyJson['error-codes'] });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// siteverify 网络故障 — 保守拒绝(防绕过),用户重试即可
|
|
36
|
+
return void errorRes(res, 503, 'CAPTCHA_VERIFY_UNAVAILABLE', '人机校验服务暂不可用,请稍后再试');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
12
39
|
const trimmed = name.trim();
|
|
13
40
|
if (trimmed.length < 2 || trimmed.length > 40)
|
|
14
41
|
return void errorRes(res, 400, 'NAME_LENGTH', '名称长度需在 2–40 个字符之间');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { buildIntentMandate, signMandate } from './ap2-mandate.js';
|
|
1
2
|
export function registerCheckoutHelpersRoutes(app, deps) {
|
|
2
|
-
const { db, auth, generateId, formatProductForAgent } = deps;
|
|
3
|
+
const { db, auth, generateId, formatProductForAgent, signPassport, issuerAddress } = deps;
|
|
3
4
|
app.get('/api/checkout/tax-preview', (req, res) => {
|
|
4
5
|
const user = auth(req, res);
|
|
5
6
|
if (!user)
|
|
@@ -43,7 +44,7 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
43
44
|
: '跨境订单 — 协议无该地区税费数据,请咨询当地海关',
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
|
-
app.post('/api/verify-price', (req, res) => {
|
|
47
|
+
app.post('/api/verify-price', async (req, res) => {
|
|
47
48
|
const user = auth(req, res);
|
|
48
49
|
if (!user)
|
|
49
50
|
return;
|
|
@@ -71,6 +72,25 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
71
72
|
INSERT INTO price_sessions (token, product_id, user_id, price, quantity, created_at, expires_at)
|
|
72
73
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
73
74
|
`).run(token, product_id, user.id, product.price, qty, now.toISOString(), expiresAt.toISOString());
|
|
75
|
+
// AP2 (B.4 b) — Intent Mandate 并存输出;不破坏现有 session_token
|
|
76
|
+
let ap2_intent_mandate = null;
|
|
77
|
+
try {
|
|
78
|
+
const out = buildIntentMandate({
|
|
79
|
+
issuerDid: 'did:web:webaz.xyz',
|
|
80
|
+
issuerAddress: issuerAddress(),
|
|
81
|
+
issuedAt: now.toISOString(),
|
|
82
|
+
expiresAt: expiresAt.toISOString(),
|
|
83
|
+
principal: { role: 'user', id: user.id },
|
|
84
|
+
productId: product_id,
|
|
85
|
+
productName: typeof product.title === 'string' ? product.title : undefined,
|
|
86
|
+
quantity: qty,
|
|
87
|
+
maxUnitPrice: product.price,
|
|
88
|
+
currency: 'WAZ',
|
|
89
|
+
sessionToken: token,
|
|
90
|
+
});
|
|
91
|
+
ap2_intent_mandate = await signMandate(out, signPassport);
|
|
92
|
+
}
|
|
93
|
+
catch { /* AP2 副输出失败不阻断主流程 */ }
|
|
74
94
|
res.json({
|
|
75
95
|
session_token: token,
|
|
76
96
|
verified_price: product.price,
|
|
@@ -80,6 +100,7 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
80
100
|
expires_at: expiresAt.toISOString(),
|
|
81
101
|
expires_in_seconds: 600,
|
|
82
102
|
note: '此价格在10分钟内有效。下单时传入 session_token 可保证此价格不变。',
|
|
103
|
+
ap2_intent_mandate,
|
|
83
104
|
});
|
|
84
105
|
});
|
|
85
106
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { buildCartMandate, buildPaymentMandate, signMandate } from './ap2-mandate.js';
|
|
1
2
|
export function registerOrdersCreateRoutes(app, deps) {
|
|
2
|
-
const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent } = deps;
|
|
3
|
-
app.post('/api/orders', (req, res) => {
|
|
3
|
+
const { db, auth, isTrustedRole, generateId, generateRecipientCode, DONATION_VALID_PCTS, INTERNAL_AUDITOR_ID, addHours, getActiveFlashSale, applyCouponToOrder, getProtocolParam, getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross, appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep, broadcastSystemEvent, signPassport, issuerAddress } = deps;
|
|
4
|
+
app.post('/api/orders', async (req, res) => {
|
|
4
5
|
const user = auth(req, res);
|
|
5
6
|
if (!user)
|
|
6
7
|
return;
|
|
@@ -334,6 +335,48 @@ export function registerOrdersCreateRoutes(app, deps) {
|
|
|
334
335
|
broadcastSystemEvent('order_created', '📦', `订单创建 ${orderId} · ${totalAmount} WAZ`, orderId);
|
|
335
336
|
}
|
|
336
337
|
catch { }
|
|
337
|
-
|
|
338
|
+
// AP2 (B.4 b) — Cart + Payment Mandate 双输出;签名失败不阻断主流程
|
|
339
|
+
let ap2_cart_mandate = null;
|
|
340
|
+
let ap2_payment_mandate = null;
|
|
341
|
+
try {
|
|
342
|
+
const productName = typeof product.title === 'string' ? product.title : String(product.id);
|
|
343
|
+
const cartOut = buildCartMandate({
|
|
344
|
+
issuerDid: 'did:web:webaz.xyz',
|
|
345
|
+
issuerAddress: issuerAddress(),
|
|
346
|
+
principal: { role: 'user', id: user.id },
|
|
347
|
+
orderId,
|
|
348
|
+
items: [{ sku: String(product.id), name: productName, quantity: reqQty, unit_price: basePrice, line_total: subtotal }],
|
|
349
|
+
subtotal,
|
|
350
|
+
fees: {
|
|
351
|
+
...(couponDiscount > 0 ? { coupon_discount: -couponDiscount } : {}),
|
|
352
|
+
...(insurancePremium > 0 ? { insurance: insurancePremium } : {}),
|
|
353
|
+
...(donationAmount > 0 ? { donation: donationAmount } : {}),
|
|
354
|
+
},
|
|
355
|
+
total: totalAmount,
|
|
356
|
+
currency: 'WAZ',
|
|
357
|
+
});
|
|
358
|
+
ap2_cart_mandate = await signMandate(cartOut, signPassport);
|
|
359
|
+
const payOut = buildPaymentMandate({
|
|
360
|
+
issuerDid: 'did:web:webaz.xyz',
|
|
361
|
+
issuerAddress: issuerAddress(),
|
|
362
|
+
payer: { role: 'user', id: user.id },
|
|
363
|
+
payee: { role: 'merchant', id: String(product.seller_uid) },
|
|
364
|
+
amount: totalAmount,
|
|
365
|
+
currency: 'WAZ',
|
|
366
|
+
paymentMethod: 'webaz_escrow',
|
|
367
|
+
orderId,
|
|
368
|
+
escrowReleaseCondition: 'buyer_confirms_receipt_or_auto_after_window',
|
|
369
|
+
});
|
|
370
|
+
ap2_payment_mandate = await signMandate(payOut, signPassport);
|
|
371
|
+
}
|
|
372
|
+
catch { /* AP2 mandate 失败仅影响 AP2-aware agent,主流程已成功 */ }
|
|
373
|
+
res.json({
|
|
374
|
+
success: true,
|
|
375
|
+
order_id: orderId,
|
|
376
|
+
total_amount: totalAmount,
|
|
377
|
+
auto_accepted: autoAccepted || undefined,
|
|
378
|
+
ap2_cart_mandate,
|
|
379
|
+
ap2_payment_mandate,
|
|
380
|
+
});
|
|
338
381
|
});
|
|
339
382
|
}
|
|
@@ -53,7 +53,13 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
53
53
|
app.get('/api/system-flags', (_req, res) => {
|
|
54
54
|
const requireRef = db.prepare("SELECT value FROM system_state WHERE key='require_ref_to_register'").get()?.value === '1';
|
|
55
55
|
const inviteRotation = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
|
|
56
|
-
|
|
56
|
+
// #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
|
|
57
|
+
const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
|
|
58
|
+
res.json({
|
|
59
|
+
require_ref_to_register: requireRef,
|
|
60
|
+
invite_rotation_enabled: inviteRotation,
|
|
61
|
+
turnstile_site_key: turnstileSiteKey,
|
|
62
|
+
});
|
|
57
63
|
});
|
|
58
64
|
// #1045 + #1048 整合 — 公开诚实化 manifest
|
|
59
65
|
// /.well-known/webaz-protocol.json — 标准 well-known URL,任何 HTTP 客户端可发现
|
|
@@ -83,6 +89,8 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
83
89
|
agent_passport: [
|
|
84
90
|
{
|
|
85
91
|
address: issuerAddress(),
|
|
92
|
+
did_web: 'did:web:webaz.xyz', // W3C DID method,resolve at /.well-known/did.json
|
|
93
|
+
did_legacy: 'did:webaz:' + issuerAddress(), // 原自定义形态,Phase 4 webaz_format 仍用
|
|
86
94
|
scheme: 'eip191',
|
|
87
95
|
purpose: 'Phase 4 Agent Passport signing (custodian_fingerprint + risk_score + engagement_depth + behavior_profile)',
|
|
88
96
|
active_since: issuerActiveSince,
|
|
@@ -91,6 +99,14 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
91
99
|
},
|
|
92
100
|
],
|
|
93
101
|
},
|
|
102
|
+
// 公开披露文档(#1050) — 协议层"钱怎么流"的源真理(协议外可读)
|
|
103
|
+
disclosures: {
|
|
104
|
+
economic_model: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md',
|
|
105
|
+
mlm_compliance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md',
|
|
106
|
+
agent_governance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
|
|
107
|
+
protocol_compatibility: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/PROTOCOL-COMPATIBILITY-AUDIT-2026-05-30.md',
|
|
108
|
+
changelog: 'https://github.com/seasonsagents-art/webaz/blob/main/CHANGELOG.md',
|
|
109
|
+
},
|
|
94
110
|
// 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
|
|
95
111
|
roadmap: {
|
|
96
112
|
philosophy: 'Disclose what is done + what is known-not-yet-done. We do not commit to deadlines (pre-launch). We DO commit to honest enumeration.',
|
|
@@ -99,11 +115,12 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
99
115
|
'Phase 3a-3d access control: registration rate-limit + invite-required + declared-scope reads/writes + non-Passkey writers must declare',
|
|
100
116
|
'Iron-Rule: arbitrate / vote / agent_revoke / delete_passkey / large withdraw all require live WebAuthn ceremony',
|
|
101
117
|
'Integrity disclosure: MCP descriptions + /.well-known + pre-launch banner + protocol-status endpoint',
|
|
118
|
+
'Cross-user read daily cap — distinct other-user-id per day, Passkey humans capped too (#1043, 2026-05-30)',
|
|
119
|
+
'AP2 Mandate dual-output — verify_price + place_order emit signed AP2 Intent/Cart/Payment Mandate alongside webaz format (B.4 b, 2026-05-30)',
|
|
120
|
+
'Public economic model document — docs/ECONOMIC-MODEL.md (#1050, 2026-05-30)',
|
|
102
121
|
],
|
|
103
122
|
known_next: [
|
|
104
|
-
'Cross-user read daily cap (anti-aggregation; applies to Passkey humans too) — task #1043, awaits real usage to calibrate N',
|
|
105
123
|
'Registration captcha or email verification (anti-sybil last mile) — task #1049, pre-launch follow-up',
|
|
106
|
-
'Public economic model document — task #1050, pre-launch follow-up',
|
|
107
124
|
'Phase 5 ZK privacy L2/L3 — long-term research; triggered when real cross-protocol consumers appear',
|
|
108
125
|
],
|
|
109
126
|
deliberate_deferrals: [
|
|
@@ -123,6 +140,46 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
123
140
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
124
141
|
res.json(buildProtocolManifest());
|
|
125
142
|
});
|
|
143
|
+
// W3C DID Document(B.6 b DID 短期 mapping,2026-05-30):
|
|
144
|
+
// did:web:webaz.xyz 通过 HTTPS 解析到这里(W3C did:web spec §3.2)
|
|
145
|
+
// verificationMethod 用 EcdsaSecp256k1RecoveryMethod2020 + CAIP-10 blockchainAccountId
|
|
146
|
+
// 任何标准 DID resolver(Veramo / SpruceID / KILT / web5 ...)可 GET → 解出 issuer key → 验 Phase 4 凭证签名
|
|
147
|
+
app.get('/.well-known/did.json', (_req, res) => {
|
|
148
|
+
const addr = issuerAddress();
|
|
149
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
150
|
+
res.json({
|
|
151
|
+
'@context': [
|
|
152
|
+
'https://www.w3.org/ns/did/v1',
|
|
153
|
+
'https://w3id.org/security/suites/secp256k1recovery-2020/v2',
|
|
154
|
+
],
|
|
155
|
+
id: 'did:web:webaz.xyz',
|
|
156
|
+
verificationMethod: [
|
|
157
|
+
{
|
|
158
|
+
id: 'did:web:webaz.xyz#key-1',
|
|
159
|
+
type: 'EcdsaSecp256k1RecoveryMethod2020',
|
|
160
|
+
controller: 'did:web:webaz.xyz',
|
|
161
|
+
// CAIP-10:eip155 namespace + Base mainnet chain id (8453) + 同把 hot wallet 地址(Phase 4 同款)
|
|
162
|
+
// 注:WAZ peg USDC,真链 Base/Base Sepolia,但 issuer key 是协议级,链中立,这里只标声明绑定到 Base 用于消费者发现
|
|
163
|
+
blockchainAccountId: `eip155:8453:${addr}`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
assertionMethod: ['did:web:webaz.xyz#key-1'],
|
|
167
|
+
authentication: ['did:web:webaz.xyz#key-1'],
|
|
168
|
+
// 自描述 — 让 resolver 知道这个 DID 用来签 webaz agent passport
|
|
169
|
+
service: [
|
|
170
|
+
{
|
|
171
|
+
id: 'did:web:webaz.xyz#agent-passport-endpoint',
|
|
172
|
+
type: 'WebAZAgentPassportEndpoint',
|
|
173
|
+
serviceEndpoint: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'did:web:webaz.xyz#protocol-manifest',
|
|
177
|
+
type: 'WebAZProtocolManifest',
|
|
178
|
+
serviceEndpoint: 'https://webaz.xyz/.well-known/webaz-protocol.json',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
126
183
|
app.get('/api/editor-picks', (_req, res) => {
|
|
127
184
|
const products = db.prepare(`
|
|
128
185
|
SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
|
package/dist/pwa/server.js
CHANGED
|
@@ -4981,6 +4981,50 @@ function checkMassActionCap(apiKey, action, level) {
|
|
|
4981
4981
|
}
|
|
4982
4982
|
return { ok: true };
|
|
4983
4983
|
}
|
|
4984
|
+
// #1043(补 A) 跨用户读日 cap — distinct other_user_id per day,真人也罩(只是 cap 更高,不再无限)
|
|
4985
|
+
// 触发面:只对路径里"显式带其他用户 ID"的端点计数 → /api/users/:id/* 类。
|
|
4986
|
+
// · 防"枚举/扒数据"型读取(剽窃 / 内容农场 / 用户画像批量化)
|
|
4987
|
+
// · 真人监护人(绑 Passkey) 也罩:audit "补 A" — 之前 Passkey 真人无任何 read cap,scraper 拿真人账号即绕过
|
|
4988
|
+
// · 不打 /api/nearby /api/search(地理聚合 / 关键词查),那些已被 B1 read-scope 约束
|
|
4989
|
+
const CROSS_USER_READ_DAILY_CAP = {
|
|
4990
|
+
passkey_human: 300, // 真人监护人 — 高但有上限(每天看 300 个不同用户已属异常使用)
|
|
4991
|
+
legend: 200, // 高信誉 agent
|
|
4992
|
+
quality: 100,
|
|
4993
|
+
trusted: 60,
|
|
4994
|
+
new: 30, // 新 agent — 30 个就该有声誉再说
|
|
4995
|
+
};
|
|
4996
|
+
const crossUserReadBuckets = new Map();
|
|
4997
|
+
function extractCrossUserTarget(path, currentUserId) {
|
|
4998
|
+
// /api/users/:id 或 /api/users/:id/anything → :id;同 user 不算跨;sys_* 协议账号不算
|
|
4999
|
+
// P1-4 修:允许 trailing path 缺失(/api/users/:user_id 是合法 endpoint,返回用户档案)
|
|
5000
|
+
const m = path.match(/^\/api\/users\/([^/?#]+)(?:[/?#]|$)/);
|
|
5001
|
+
if (!m)
|
|
5002
|
+
return null;
|
|
5003
|
+
const target = m[1];
|
|
5004
|
+
if (!target || target === currentUserId)
|
|
5005
|
+
return null;
|
|
5006
|
+
if (target.startsWith('sys_'))
|
|
5007
|
+
return null;
|
|
5008
|
+
return target;
|
|
5009
|
+
}
|
|
5010
|
+
function checkCrossUserReadCap(userId, targetId, level) {
|
|
5011
|
+
const today = todayKey();
|
|
5012
|
+
let bucket = crossUserReadBuckets.get(userId);
|
|
5013
|
+
if (!bucket || bucket.dayKey !== today) {
|
|
5014
|
+
bucket = { dayKey: today, distinct: new Set(), overruns: 0 };
|
|
5015
|
+
crossUserReadBuckets.set(userId, bucket);
|
|
5016
|
+
}
|
|
5017
|
+
// 已读过同一个 target 不再计数(distinct cap)
|
|
5018
|
+
if (!bucket.distinct.has(targetId))
|
|
5019
|
+
bucket.distinct.add(targetId);
|
|
5020
|
+
const cap = CROSS_USER_READ_DAILY_CAP[level] ?? CROSS_USER_READ_DAILY_CAP.new;
|
|
5021
|
+
const used = bucket.distinct.size;
|
|
5022
|
+
if (used > cap) {
|
|
5023
|
+
bucket.overruns++;
|
|
5024
|
+
return { ok: false, cap, used };
|
|
5025
|
+
}
|
|
5026
|
+
return { ok: true, cap, used };
|
|
5027
|
+
}
|
|
4984
5028
|
// 2026-05-23 P0 audit fix 2.2:endpoint → action 映射(用于 declared_scope enforcement)
|
|
4985
5029
|
// 只对写操作 enforce;读操作不限制(任何 agent 都能读自己范围内的数据)
|
|
4986
5030
|
function endpointToAction(method, path) {
|
|
@@ -5149,6 +5193,29 @@ app.use((req, res, next) => {
|
|
|
5149
5193
|
error_code: 'AGENT_RISK_READ_THROTTLED', risk: riskInfo.risk,
|
|
5150
5194
|
});
|
|
5151
5195
|
}
|
|
5196
|
+
// #1043(补 A) 跨用户读日 cap — 真人也罩;只对路径里显式带 other user id 的 GET 计数
|
|
5197
|
+
// P0-2 优化:先 regex 快判 path 命中(零开销),命中才查 owner.id + level — 避免每 GET 跑 SQLite
|
|
5198
|
+
// P1-4 修:正则允许 /api/users/:id 无 trailing slash(GET /api/users/:user_id 是返回用户档案的合法端点)
|
|
5199
|
+
if (req.method === 'GET' && /^\/api\/users\/[^/?#]+/.test(req.path)) {
|
|
5200
|
+
const owner = db.prepare(`SELECT id FROM users WHERE api_key = ?`).get(apiKey);
|
|
5201
|
+
if (owner) {
|
|
5202
|
+
const target = extractCrossUserTarget(req.path, owner.id);
|
|
5203
|
+
if (target) {
|
|
5204
|
+
const lvl = riskInfo.hasPasskey
|
|
5205
|
+
? 'passkey_human'
|
|
5206
|
+
: (db.prepare(`SELECT level FROM agent_reputation WHERE api_key = ?`).get(apiKey)?.level || 'new');
|
|
5207
|
+
const cr = checkCrossUserReadCap(owner.id, target, lvl);
|
|
5208
|
+
if (!cr.ok) {
|
|
5209
|
+
res.setHeader('Retry-After', '600');
|
|
5210
|
+
return void res.status(429).json({
|
|
5211
|
+
error: `今日跨用户读已达上限 ${cr.cap}(${lvl})。改天再来,或为该 agent 收窄 declared_scope`,
|
|
5212
|
+
error_code: 'CROSS_USER_READ_DAILY_CAP',
|
|
5213
|
+
cap: cr.cap, used: cr.used, level: lvl,
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5152
5219
|
// 分档 rate limit
|
|
5153
5220
|
const repRow = db.prepare(`SELECT level FROM agent_reputation WHERE api_key = ?`).get(apiKey);
|
|
5154
5221
|
const level = repRow?.level || 'new';
|
|
@@ -7187,6 +7254,8 @@ registerOrdersCreateRoutes(app, {
|
|
|
7187
7254
|
getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross,
|
|
7188
7255
|
appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep,
|
|
7189
7256
|
broadcastSystemEvent,
|
|
7257
|
+
signPassport: (message) => privateKeyToAccount(derivePrivKey('platform-hot-wallet')).signMessage({ message }),
|
|
7258
|
+
issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
|
|
7190
7259
|
});
|
|
7191
7260
|
// #1013 Phase 86: 5 disputes 读端点已迁出
|
|
7192
7261
|
registerDisputesReadRoutes(app, {
|
|
@@ -8221,7 +8290,11 @@ registerAnchorsRoutes(app, { db, auth, rateLimitOk });
|
|
|
8221
8290
|
registerExternalAnchorsRoutes(app, { db, auth });
|
|
8222
8291
|
// GET /api/orders/:id/chain + GET /api/orders/:id — Phase 83 已迁出
|
|
8223
8292
|
// #1013 Phase 109: checkout/tax-preview + verify-price 已迁出
|
|
8224
|
-
registerCheckoutHelpersRoutes(app, {
|
|
8293
|
+
registerCheckoutHelpersRoutes(app, {
|
|
8294
|
+
db, auth, generateId, formatProductForAgent,
|
|
8295
|
+
signPassport: (message) => privateKeyToAccount(derivePrivKey('platform-hot-wallet')).signMessage({ message }),
|
|
8296
|
+
issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
|
|
8297
|
+
});
|
|
8225
8298
|
// ─── M8 二手板块 ────────────────────────────────────────────
|
|
8226
8299
|
// #1013 Phase 27: 6 endpoints + 4 SH_* sets + addHours 已迁出到 routes/secondhand.ts
|
|
8227
8300
|
registerSecondhandRoutes(app, { db, generateId, auth, errorRes });
|
package/package.json
CHANGED