@seasonkoh/webaz 0.1.30 → 0.1.31
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 +69 -17
- package/dist/pwa/public/openapi.json +11 -1
- package/dist/pwa/routes/agent-grants.js +30 -2
- package/dist/pwa/routes/products-create.js +33 -13
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +4 -4
- package/package.json +3 -1
|
@@ -308,22 +308,21 @@ export async function handlePair(args) {
|
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
310
|
if (action === 'verify') {
|
|
311
|
-
//
|
|
312
|
-
// it re-checks active/expiry/revoked/subject-suspension
|
|
313
|
-
// bearer from the secret store and attach it; the raw token is never printed.
|
|
314
|
-
// business tool and no risk scope is wired to grants here.
|
|
311
|
+
// Ask the server for the FULL grant (all authorized scopes/bundle/expiry/status), not just one capability.
|
|
312
|
+
// The SERVER is authoritative: it re-checks active/expiry/revoked/subject-suspension + audits on EVERY call.
|
|
313
|
+
// We resolve the bearer from the secret store and attach it; the raw token is never printed.
|
|
315
314
|
const cred = resolveGrantCredential();
|
|
316
315
|
if (!cred)
|
|
317
316
|
return { status: 'not_paired', error_code: 'NO_GRANT_CREDENTIAL', hint: 'No stored grant. Run webaz_pair action="start", have the human approve, then action="complete".' };
|
|
318
|
-
const resp = await apiCall('/api/agent-grants/
|
|
317
|
+
const resp = await apiCall('/api/agent-grants/verify', { method: 'GET', apiKey: cred.token });
|
|
319
318
|
if (resp.error) {
|
|
320
319
|
return { status: 'grant_invalid', grant_id: cred.grant_id, error: resp.error, error_code: resp.error_code, hint: 'Grant is no longer valid (revoked / expired / suspended). Re-pair with webaz_pair action="start".' };
|
|
321
320
|
}
|
|
322
321
|
return {
|
|
323
322
|
status: 'active',
|
|
324
|
-
grant: resp.grant, // SERVER-AUTHORITATIVE principal
|
|
323
|
+
grant: resp.grant, // SERVER-AUTHORITATIVE principal: grant_id/human_id/agent_label + ALL scopes[]/permission_bundle/expiry/status
|
|
325
324
|
local_cache: { capabilities: cred.capabilities, expires_at: cred.expires_at }, // advisory only, may be stale — NOT authoritative
|
|
326
|
-
note: 'Grant verified LIVE by the server (per-call: active/expiry/revoked/subject-suspension
|
|
325
|
+
note: 'Grant verified LIVE by the server (per-call: active/expiry/revoked/subject-suspension + audited). `grant.scopes` is the authoritative full scope list; `local_cache` is advisory. Safe scopes only.',
|
|
327
326
|
};
|
|
328
327
|
}
|
|
329
328
|
if (action === 'request') {
|
|
@@ -654,7 +653,7 @@ Skipping is allowed but agent then carries price/stock-race risk itself.`,
|
|
|
654
653
|
// was ~1336 chars, now ~650 chars
|
|
655
654
|
description: `⚠️ **"list" = PUBLISH** (verb), NOT "list out". Seller-only catalog publish + manage.
|
|
656
655
|
|
|
657
|
-
USE THIS when seller wants to publish / update / delist / relist / trash / delete own product, OR view own listings (action=mine). NOT for browsing marketplace — use webaz_search (anyone, no auth).
|
|
656
|
+
USE THIS when seller wants to publish / update / delist / relist / trash / delete own product, OR view own listings (action=mine). NOT for browsing marketplace — use webaz_search (anyone, no auth). A paired delegation grant (webaz_pair — Catalog Agent) can **read** (action=mine, seller_products_read) and **create DRAFTS** (action=create, seller_product_draft → the product lands in your warehouse as status=warehouse, NOT public); **publishing (going active), updating, delisting and deleting stay human/api_key only** — a grant can never publish. A grant that lacks the scope gets a structured PERMISSION_REQUIRED (request it via webaz_pair action="request", then retry). On create: system auto-suggests stake ~15% of price (buyer protection).
|
|
658
657
|
|
|
659
658
|
Fill agent_summary fields completely (brand/return/handling/warranty) — helps buyer agents compare. For "exclusive-price vs external-link" listing → PWA Web only (link claim needs crowd-verify).
|
|
660
659
|
|
|
@@ -671,7 +670,7 @@ Actions: create (title/description/price) | mine | update (product_id + changed
|
|
|
671
670
|
product_id: { type: 'string', description: 'Product ID (required for update/delist/relist/trash/delete)' },
|
|
672
671
|
title: { type: 'string', description: 'Product name (required for create; optional for update)' },
|
|
673
672
|
description: { type: 'string', description: 'Product description (required for create; optional for update)' },
|
|
674
|
-
price: { type: 'number', description: '
|
|
673
|
+
price: { type: 'number', description: 'Listing amount = the protocol/base amount (waz_usdc_rate 1.0). No currency is set at listing time. On the direct_p2p rail the actual payable DISPLAY follows the seller\'s declared receiving-account currency/instruction — USDC only when that account is USDC/USD, otherwise the seller\'s currency ("以收款说明为准"); WebAZ does not custody, route, validate, or restrict the payment method/currency. USDC is only the base/reference amount when applicable. Legacy escrow uses WAZ. Required for create; optional for update.' },
|
|
675
674
|
stock: { type: 'number', description: 'Stock, default 1' },
|
|
676
675
|
category: { type: 'string', description: 'Category (optional)' },
|
|
677
676
|
specs: {
|
|
@@ -724,9 +723,14 @@ Actions: create (title/description/price) | mine | update (product_id + changed
|
|
|
724
723
|
{
|
|
725
724
|
name: 'webaz_place_order',
|
|
726
725
|
// was ~1117 chars, now ~580 chars
|
|
727
|
-
description: `Buyer places order. Buyer api_key required.
|
|
726
|
+
description: `Buyer places order. Buyer api_key required.
|
|
728
727
|
|
|
729
|
-
|
|
728
|
+
Payment rail (chosen here, at purchase — default: escrow):
|
|
729
|
+
- **escrow** (default, legacy): funds auto-enter protocol escrow; the amount is the WAZ/protocol-unit price. The deadlines/auto-judge below apply.
|
|
730
|
+
- **direct_p2p** (live, non-custodial direct pay): buyer pays the seller directly OFF-platform via the seller's chosen receiving account. The order amount is the protocol/base amount; the actual payable DISPLAY follows that account's declared currency/instruction (USDC only when the account is USDC/USD — otherwise the seller's currency, "以收款说明为准"). WebAZ does NOT custody, route, validate, or restrict the method/currency. Pass \`direct_receive_account_id\` (seller's receiving account). Eligibility — product/store verification, per-tx caps, valid account — is enforced by the SERVER; you do not decide it. Ineligible → the route's own gate error (e.g. DIRECT_PAY_PRODUCT_NOT_VERIFIED → use escrow).
|
|
731
|
+
- onchain_full_stake / psp: PLANNED, not enabled → passing them returns PAYMENT_RAIL_DISABLED.
|
|
732
|
+
|
|
733
|
+
Order deadlines (escrow rail; absolute ISO timestamps in response):
|
|
730
734
|
- accept T+48h | ship T+120h (72h after accept)
|
|
731
735
|
- pickup T+168h (48h after ship) | delivery T+336h (7d after pickup)
|
|
732
736
|
- confirm T+408h (72h after delivery)
|
|
@@ -743,6 +747,8 @@ Options:
|
|
|
743
747
|
api_key: { type: 'string', description: "Buyer's api_key (or omit and set the WEBAZ_API_KEY env var)" },
|
|
744
748
|
product_id: { type: 'string', description: 'Product ID to buy (from webaz_search)' },
|
|
745
749
|
quantity: { type: 'number', description: 'Quantity, default 1' },
|
|
750
|
+
payment_rail: { type: 'string', enum: ['escrow', 'direct_p2p'], description: 'Payment rail (default: escrow). "direct_p2p" = live non-custodial direct pay, off-platform; the payable display follows the seller\'s receiving-account currency/instruction (USDC only as the base/reference amount, WebAZ doesn\'t custody/route/validate/restrict currency) — requires direct_receive_account_id; the server runs the direct-pay eligibility gates (you do not). Other rails (onchain_full_stake / psp) are planned but disabled → PAYMENT_RAIL_DISABLED.' },
|
|
751
|
+
direct_receive_account_id: { type: 'string', description: 'For payment_rail="direct_p2p": the seller\'s direct-receive account id to pay. Ignored on escrow.' },
|
|
746
752
|
shipping_address: { type: 'string', description: 'Shipping address' },
|
|
747
753
|
notes: { type: 'string', description: 'Note to seller (optional)' },
|
|
748
754
|
session_token: {
|
|
@@ -1584,7 +1590,7 @@ Enums: **category** phone/computer/appliance/furniture/clothing/book/toy/sports/
|
|
|
1584
1590
|
description: { type: 'string', description: 'Description (≤1000 chars, optional)' },
|
|
1585
1591
|
category: { type: 'string', enum: ['phone', 'computer', 'appliance', 'furniture', 'clothing', 'book', 'toy', 'sports', 'other'], description: 'Category (required for publish)' },
|
|
1586
1592
|
condition_grade: { type: 'string', enum: ['brand_new', 'like_new', 'lightly_used', 'well_used', 'heavily_used'], description: 'Condition grade (required for publish)' },
|
|
1587
|
-
price: { type: 'number', description: '
|
|
1593
|
+
price: { type: 'number', description: 'Listing amount 0-100000 = protocol/base amount (waz_usdc_rate 1.0). On direct_p2p the payable display follows the seller\'s receiving-account currency/instruction (USDC only as the base/reference; WebAZ doesn\'t custody/route/validate/restrict currency); WAZ on legacy escrow. Rail chosen at purchase. Required for publish.' },
|
|
1588
1594
|
negotiable: { type: 'boolean', description: 'Negotiable flag (optional)' },
|
|
1589
1595
|
images: { type: 'array', items: { type: 'string' }, description: 'Images dataURL/URL array, ≥1 ≤9 (required for publish)' },
|
|
1590
1596
|
region: { type: 'string', description: 'Region (≤40 chars, optional)' },
|
|
@@ -2442,12 +2448,43 @@ async function handleVerifyPrice(args) {
|
|
|
2442
2448
|
next: `调用 webaz_place_order 时传入 session_token="${token}" 确保以此价格成交`,
|
|
2443
2449
|
};
|
|
2444
2450
|
}
|
|
2445
|
-
async function handleListProduct(args) {
|
|
2451
|
+
export async function handleListProduct(args) {
|
|
2446
2452
|
// Wave 3 audit P0: 加 action 分发 — agent 卖家能完整管理目录(不止 create)
|
|
2447
2453
|
const action = args.action || 'create';
|
|
2448
2454
|
const apiKey = resolveMcpApiKey(args);
|
|
2449
|
-
if (!apiKey)
|
|
2450
|
-
|
|
2455
|
+
if (!apiKey) {
|
|
2456
|
+
// No human api_key — fall back to a delegation grant (Catalog Agent). A grant can READ the catalog
|
|
2457
|
+
// (action="mine", seller_products_read) and create DRAFTS (action="create"|"draft", seller_product_draft
|
|
2458
|
+
// → status=warehouse, NOT public). Publishing (active) + all other writes stay human/api_key only.
|
|
2459
|
+
// Missing scope → structured PERMISSION_REQUIRED (agent can request it, then retry).
|
|
2460
|
+
if (!isNetworkMode())
|
|
2461
|
+
return { error: 'a delegation grant requires NETWORK mode (grants live on webaz.xyz)', error_code: 'GRANT_REQUIRES_NETWORK' };
|
|
2462
|
+
const cred = resolveGrantCredential();
|
|
2463
|
+
if (!cred)
|
|
2464
|
+
return { error: 'api_key required — or pair a delegation grant: webaz_pair action="start", have the human approve the catalog_agent bundle, then retry.', error_code: 'AUTH_REQUIRED' };
|
|
2465
|
+
if (action === 'mine') {
|
|
2466
|
+
const r = await apiCall('/api/agent/seller/products', { method: 'GET', apiKey: cred.token });
|
|
2467
|
+
if (r.error_code === 'PERMISSION_REQUIRED')
|
|
2468
|
+
return { ...r, retry_after_approval: true, hint: 'Your grant lacks seller_products_read. Run webaz_pair action="request" bundle="catalog_agent", have the human approve, then retry.' };
|
|
2469
|
+
if (r.error)
|
|
2470
|
+
return r;
|
|
2471
|
+
return { found: r.count, products: r.products, seller_id: r.seller_id, via: 'delegation_grant', note: 'Read via your delegation grant (safe scope seller_products_read). Creating a DRAFT: action="create" (goes to your warehouse for you to review + publish).' };
|
|
2472
|
+
}
|
|
2473
|
+
if (action === 'create' || action === 'draft') {
|
|
2474
|
+
const createFields = ['title', 'description', 'price', 'stock', 'category', 'specs', 'brand', 'model', 'source_url', 'source_price', 'external_title', 'weight_kg', 'ship_regions', 'handling_hours', 'estimated_days', 'fragile', 'return_days', 'return_condition', 'warranty_days', 'commission_rate', 'product_type', 'aliases', 'image_hashes', 'additional_links'];
|
|
2475
|
+
const body = {};
|
|
2476
|
+
for (const k of createFields)
|
|
2477
|
+
if (args[k] !== undefined)
|
|
2478
|
+
body[k] = args[k];
|
|
2479
|
+
const r = await apiCall('/api/agent/seller/products', { method: 'POST', apiKey: cred.token, body });
|
|
2480
|
+
if (r.error_code === 'PERMISSION_REQUIRED')
|
|
2481
|
+
return { ...r, retry_after_approval: true, hint: 'Your grant lacks seller_product_draft. Run webaz_pair action="request" bundle="catalog_agent", have the human approve, then retry.' };
|
|
2482
|
+
if (r.error)
|
|
2483
|
+
return r;
|
|
2484
|
+
return { ...r, via: 'delegation_grant', note: 'Created as a DRAFT (status=warehouse — NOT public/sellable). Publishing stays with the human: they review it in 我的商品 → 仓库 and publish (a notification was sent). A grant can never publish.' };
|
|
2485
|
+
}
|
|
2486
|
+
return { error_code: 'GRANT_WRITE_NOT_ENABLED', note: `action="${action}" is not available via a delegation grant — a grant can READ (action="mine") and create DRAFTS (action="create" → warehouse). Publishing, updating, delisting, and deleting stay human/api_key only. Provide a seller api_key for those.` };
|
|
2487
|
+
}
|
|
2451
2488
|
// RFC-003 P2b: NETWORK 模式 — 卖家目录管理全部转发生产端点(单一真相源)
|
|
2452
2489
|
if (toolBackend('webaz_list_product') === 'network') {
|
|
2453
2490
|
const pid = args.product_id;
|
|
@@ -2610,9 +2647,20 @@ async function handleListProduct(args) {
|
|
|
2610
2647
|
...(extraResult ? { extra_fields_applied: !('error' in extraResult), extra_result: extraResult } : {}),
|
|
2611
2648
|
};
|
|
2612
2649
|
}
|
|
2613
|
-
async function handlePlaceOrder(args) {
|
|
2650
|
+
export async function handlePlaceOrder(args) {
|
|
2651
|
+
// Payment rail (PWA-aligned). Only escrow (legacy) + direct_p2p (live, non-custodial) are enabled; the
|
|
2652
|
+
// other declared rails (onchain_full_stake / psp) are PLANNED placeholders — refuse them here, never
|
|
2653
|
+
// silently downgrade. The MCP does NOT judge direct-pay eligibility: it forwards payment_rail +
|
|
2654
|
+
// direct_receive_account_id to the SAME /api/orders route, which runs all the PWA gates.
|
|
2655
|
+
const rail = args.payment_rail;
|
|
2656
|
+
if (rail != null && rail !== 'escrow' && rail !== 'direct_p2p') {
|
|
2657
|
+
return { error: `payment rail "${String(rail)}" is planned but not enabled — only "direct_p2p" (live, non-custodial direct pay) and "escrow" (legacy) are available.`, error_code: 'PAYMENT_RAIL_DISABLED' };
|
|
2658
|
+
}
|
|
2659
|
+
if (rail === 'direct_p2p' && !isNetworkMode()) {
|
|
2660
|
+
return { error: 'direct_p2p (direct pay) requires NETWORK mode — it settles against a real seller receiving account on webaz.xyz.', error_code: 'DIRECT_PAY_REQUIRES_NETWORK' };
|
|
2661
|
+
}
|
|
2614
2662
|
// RFC-003 P2: network 模式转发到生产 POST /api/orders(前置,绕过本地 db)。
|
|
2615
|
-
// 生产端做完整鉴权/库存/session/spend-cap
|
|
2663
|
+
// 生产端做完整鉴权/库存/session/spend-cap/结算 + direct-pay gate(createDirectPayResponse)。
|
|
2616
2664
|
if (toolBackend('webaz_place_order') === 'network') {
|
|
2617
2665
|
const body = { product_id: args.product_id, quantity: Number(args.quantity ?? 1) };
|
|
2618
2666
|
if (args.session_token != null)
|
|
@@ -2623,6 +2671,10 @@ async function handlePlaceOrder(args) {
|
|
|
2623
2671
|
body.shipping_address = args.shipping_address;
|
|
2624
2672
|
if (args.donation_pct != null)
|
|
2625
2673
|
body.donation_pct = args.donation_pct;
|
|
2674
|
+
if (args.payment_rail != null)
|
|
2675
|
+
body.payment_rail = args.payment_rail; // escrow | direct_p2p (route forks + gates)
|
|
2676
|
+
if (args.direct_receive_account_id != null)
|
|
2677
|
+
body.direct_receive_account_id = args.direct_receive_account_id; // seller receiving acct (direct_p2p)
|
|
2626
2678
|
return apiCall('/api/orders', { method: 'POST', apiKey: resolveMcpApiKey(args), body });
|
|
2627
2679
|
}
|
|
2628
2680
|
const auth = requireAuth(db, resolveMcpApiKey(args));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "WebAZ Protocol API",
|
|
5
5
|
"version": "0.4.14",
|
|
6
|
-
"description": "Auto-generated endpoint inventory (
|
|
6
|
+
"description": "Auto-generated endpoint inventory (777 endpoints, 17 with full schema). See docs/ for design context."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
9
9
|
{
|
|
@@ -2774,6 +2774,16 @@
|
|
|
2774
2774
|
}
|
|
2775
2775
|
],
|
|
2776
2776
|
"x-webaz-grant-scope": "seller_products_read"
|
|
2777
|
+
},
|
|
2778
|
+
"post": {
|
|
2779
|
+
"summary": "POST /api/agent/seller/products",
|
|
2780
|
+
"tags": [],
|
|
2781
|
+
"security": [
|
|
2782
|
+
{
|
|
2783
|
+
"grantBearer": []
|
|
2784
|
+
}
|
|
2785
|
+
],
|
|
2786
|
+
"x-webaz-grant-scope": "seller_product_draft"
|
|
2777
2787
|
}
|
|
2778
2788
|
},
|
|
2779
2789
|
"/api/agent/verifiability": {
|
|
@@ -32,7 +32,7 @@ function consentView(p) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export function registerAgentGrantsRoutes(app, deps) {
|
|
35
|
-
const { db, auth, generateId, rateLimitOk, requireHumanPresence } = deps;
|
|
35
|
+
const { db, auth, generateId, rateLimitOk, requireHumanPresence, createProductDraftHandler } = deps;
|
|
36
36
|
// PWA runtime self-init (MCP gets the tables via applyWebazRuntimeSchema). Idempotent.
|
|
37
37
|
initAgentDelegationGrantsSchema(db);
|
|
38
38
|
initAgentPairingSchema(db);
|
|
@@ -86,7 +86,9 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
86
86
|
error: `this action needs the "${scope}" permission, which your grant does not carry`,
|
|
87
87
|
error_code: 'PERMISSION_REQUIRED',
|
|
88
88
|
required_scope: scope,
|
|
89
|
+
missing_scopes: [scope],
|
|
89
90
|
approval_url: '/#agent-approvals',
|
|
91
|
+
retry_after_approval: true,
|
|
90
92
|
request_permission: { method: 'POST', endpoint: '/api/agent-grants/permission-requests', body: { scopes: [scope] } },
|
|
91
93
|
note: 'Ask the human to approve at approval_url; on approval your existing grant is expanded — then retry this request.',
|
|
92
94
|
});
|
|
@@ -115,6 +117,32 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
115
117
|
const rows = await dbAll("SELECT id, title, status, price, currency, stock, category, created_at, updated_at FROM products WHERE seller_id = ? AND status != 'deleted' ORDER BY created_at DESC LIMIT 200", [p.human_id]);
|
|
116
118
|
res.json({ seller_id: p.human_id, agent_label: p.agent_label, count: rows.length, products: rows, note: 'Seller-owned catalog read via delegation grant (safe scope seller_products_read). Read-only; no money/commission fields.' });
|
|
117
119
|
});
|
|
120
|
+
// POST create a DRAFT product via a delegation grant (Catalog Agent, safe scope seller_product_draft). The
|
|
121
|
+
// draft is FORCED to status='warehouse' (not public/sellable). PUBLISHING STAYS HUMAN-ONLY — the human
|
|
122
|
+
// flips warehouse→active in the existing 我的商品→仓库 UI (publish is never delegated to a grant, matching
|
|
123
|
+
// the taxonomy). Reuses the SAME product-create validation as the human POST /api/products
|
|
124
|
+
// (createProductDraftHandler) so the agent path can't drift. Audited by the middleware. Lightweight signal:
|
|
125
|
+
// a notification tells the human a draft is waiting to review + publish.
|
|
126
|
+
if (createProductDraftHandler) {
|
|
127
|
+
app.post('/api/agent/seller/products', requireAgentGrantScope('seller_product_draft'), async (req, res) => {
|
|
128
|
+
const p = req.agentGrant;
|
|
129
|
+
const human = await dbOne('SELECT id, role FROM users WHERE id = ?', [p.human_id]);
|
|
130
|
+
if (!human || human.role !== 'seller')
|
|
131
|
+
return void res.status(403).json({ error: 'the grant owner is not a seller — product drafts need a seller account', error_code: 'NOT_A_SELLER' });
|
|
132
|
+
await createProductDraftHandler(req, res, human, {
|
|
133
|
+
forceStatus: 'warehouse',
|
|
134
|
+
skipExternalLinkEffects: true, // a SAFE draft grant must never trigger wallet debit / verify_tasks / auto-verified links (source_url stays inert metadata)
|
|
135
|
+
onCreated: async (productId) => {
|
|
136
|
+
try {
|
|
137
|
+
await dbRun("INSERT INTO notifications (id, user_id, type, title, body) VALUES (?,?,?,?,?)", [generateId('ntf'), p.human_id, 'agent_product_draft', 'AI 助手起草了商品草稿', `${p.agent_label || 'An agent'} 起草了商品草稿 ${productId},请到「我的商品 → 仓库」审核并发布。`]);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
console.error('[agent-grant draft notify]', e.message);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
118
146
|
// helpers for the permission-request flow ----------------------------------------------------------------
|
|
119
147
|
const parseCapList = (json) => { try {
|
|
120
148
|
const a = JSON.parse(json);
|
|
@@ -149,7 +177,7 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
149
177
|
// Fail closed: a grant-authorized read is audited or it does not proceed (parity with requireAgentGrantScope).
|
|
150
178
|
if (!(await auditGrant(g.grant_id, g.human_id, 'grant:verify', 'allow')))
|
|
151
179
|
return void res.status(503).json({ error: 'authorization audit unavailable; refusing to proceed unaudited', error_code: 'GRANT_AUDIT_FAILED' });
|
|
152
|
-
res.json({ grant: { grant_id: full.grant_id, agent_label: full.agent_label, scopes: scopeNames(full.capabilities), permission_bundle: full.permission_bundle, expires_at: full.expires_at, status: full.status }, note: 'Full grant principal — scopes/bundle/expiry/status. Not a human session; never authorizes risk/never-delegable actions.' });
|
|
180
|
+
res.json({ grant: { grant_id: full.grant_id, human_id: full.human_id, agent_label: full.agent_label, scopes: scopeNames(full.capabilities), permission_bundle: full.permission_bundle, expires_at: full.expires_at, status: full.status }, note: 'Full grant principal — all authorized scopes/bundle/expiry/status. Not a human session; never authorizes risk/never-delegable actions.' });
|
|
153
181
|
});
|
|
154
182
|
// POST create a permission request — the AGENT (holding its current grant) asks for MORE scope / a bundle.
|
|
155
183
|
// Bound to (human_id, grant_id) from the grant bearer. Rate-limited. Safe-only: risk/never-delegable are
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The SINGLE source of product-create validation + insert. res-coupled but auth-agnostic: the caller resolves
|
|
4
|
+
* `user` (the human api_key route, OR the delegation-grant draft route) and may force `warehouse` status. Both
|
|
5
|
+
* the human `POST /api/products` and the grant-gated draft route MUST go through this — no parallel copy.
|
|
6
|
+
*/
|
|
7
|
+
export function makeCreateProductHandler(deps) {
|
|
8
|
+
const { db, generateId, checkSellerCanList, getStakeDiscount, VALID_PRODUCT_TYPES, parsePlatformUrl, makeCommitmentHash, makeDescriptionHash, makePriceHash } = deps;
|
|
9
|
+
return async function createProductHandler(req, res, user, opts = {}) {
|
|
10
10
|
// 里程碑 3-D:1h 上架限速(防 spam 批量上架)
|
|
11
11
|
const LISTING_RATE_LIMIT = 5;
|
|
12
12
|
const recentListings = (await dbOne(`
|
|
@@ -55,7 +55,7 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
55
55
|
return void res.json({ error: 'commission_rate 必须在 1% - 50% 之间(小数 0.01-0.50)' });
|
|
56
56
|
}
|
|
57
57
|
// 上架前检查:同一卖家不能重复关联相同外部链接
|
|
58
|
-
if (source_url) {
|
|
58
|
+
if (source_url && !opts.skipExternalLinkEffects) {
|
|
59
59
|
const sameSellerDupe = (await dbOne(`
|
|
60
60
|
SELECT COUNT(*) as n FROM product_external_links pel
|
|
61
61
|
JOIN products p ON pel.product_id = p.id
|
|
@@ -94,14 +94,14 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
94
94
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'WAZ',?,?,?,?,?,?)`, [
|
|
95
95
|
id, user.id, title, description, priceNum, Number(stock), category, stakeAmount,
|
|
96
96
|
specsJson, brand ?? null, model ?? null,
|
|
97
|
-
source_url ?? null, source_price ? Number(source_price) : null, source_price ? now : null,
|
|
97
|
+
opts.skipExternalLinkEffects ? null : (source_url ?? null), opts.skipExternalLinkEffects ? null : (source_price ? Number(source_price) : null), opts.skipExternalLinkEffects ? null : (source_price ? now : null),
|
|
98
98
|
weight_kg ? Number(weight_kg) : null, ship_regions, Number(handling_hours), estJson, fragile ? 1 : 0,
|
|
99
99
|
Number(return_days), return_condition, Number(warranty_days),
|
|
100
100
|
Math.max(0, Math.floor(Number(low_stock_threshold) || 0)), auto_delist_on_zero ? 1 : 0,
|
|
101
101
|
makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }),
|
|
102
102
|
makePriceHash(priceNum, now), now,
|
|
103
103
|
commissionRateNum, product_type, imagesJsonForInsert,
|
|
104
|
-
_tx(package_size, 40), _cc(origin_country), _cc(country_of_origin), _tx(customs_description, 120), _hs, create_status === 'warehouse' ? 'warehouse' : 'active'
|
|
104
|
+
_tx(package_size, 40), _cc(origin_country), _cc(country_of_origin), _tx(customs_description, 120), _hs, (opts.forceStatus === 'warehouse' || create_status === 'warehouse') ? 'warehouse' : 'active'
|
|
105
105
|
]);
|
|
106
106
|
// M7.2.6:免质押上架 — 不再扣 stake;首单成交时 settleOrder 自动从订单 escrow 锁定
|
|
107
107
|
// M7.2-6: 上架时同步入 aliases(卖家已勾选的 candidates)
|
|
@@ -128,7 +128,7 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
128
128
|
}
|
|
129
129
|
// 来源链接:冲突检测
|
|
130
130
|
let linkConflict = null;
|
|
131
|
-
if (source_url) {
|
|
131
|
+
if (source_url && !opts.skipExternalLinkEffects) {
|
|
132
132
|
// 另一家卖家已认领此链接(verified=1)
|
|
133
133
|
const otherClaim = await dbOne(`
|
|
134
134
|
SELECT pel.product_id FROM product_external_links pel
|
|
@@ -183,7 +183,7 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
183
183
|
// 额外链接:同步冲突检查(最多 5 个)
|
|
184
184
|
const additionalLinks = req.body.additional_links;
|
|
185
185
|
const blockedLinks = [];
|
|
186
|
-
if (Array.isArray(additionalLinks) && additionalLinks.length > 0) {
|
|
186
|
+
if (!opts.skipExternalLinkEffects && Array.isArray(additionalLinks) && additionalLinks.length > 0) {
|
|
187
187
|
for (const extraUrl of additionalLinks.slice(0, 5)) {
|
|
188
188
|
if (typeof extraUrl !== 'string' || !extraUrl.startsWith('http'))
|
|
189
189
|
continue;
|
|
@@ -220,13 +220,33 @@ export function registerProductsCreateRoutes(app, deps) {
|
|
|
220
220
|
catch { }
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
if (opts.onCreated) {
|
|
224
|
+
try {
|
|
225
|
+
await opts.onCreated(id);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.error('[products-create onCreated]', e.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
223
231
|
res.json({
|
|
224
232
|
success: true,
|
|
225
233
|
product_id: id,
|
|
234
|
+
status: (opts.forceStatus === 'warehouse' || create_status === 'warehouse') ? 'warehouse' : 'active',
|
|
226
235
|
stake_locked: 0, // 免质押上架(M7.2.6 方案 3)
|
|
227
236
|
stake_deferred: stakeAmount, // 首单成交时自动锁定(trusted+ 跳过)
|
|
228
237
|
...(linkConflict ? { link_conflict: linkConflict } : {}),
|
|
229
238
|
...(blockedLinks.length > 0 ? { blocked_links: blockedLinks } : {}),
|
|
230
239
|
});
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export function registerProductsCreateRoutes(app, deps) {
|
|
243
|
+
const createProductHandler = makeCreateProductHandler(deps);
|
|
244
|
+
app.post('/api/products', async (req, res) => {
|
|
245
|
+
const user = deps.auth(req, res);
|
|
246
|
+
if (!user)
|
|
247
|
+
return;
|
|
248
|
+
if (user.role !== 'seller')
|
|
249
|
+
return void res.json({ error: '仅卖家可上架商品' });
|
|
250
|
+
await createProductHandler(req, res, user);
|
|
231
251
|
});
|
|
232
252
|
}
|
|
@@ -196,5 +196,5 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
196
196
|
// RFC-020 PR-B — agent delegation grants (issue/read/revoke). Co-registered with the
|
|
197
197
|
// Passkey security routes so the money-dense server.ts stays untouched. Safe scopes
|
|
198
198
|
// only; risk scopes default-hard-reject. Reuses db/auth/generateId from WebauthnDeps.
|
|
199
|
-
registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk, requireHumanPresence });
|
|
199
|
+
registerAgentGrantsRoutes(app, { db, auth, generateId, rateLimitOk, requireHumanPresence, createProductDraftHandler: deps.createProductDraftHandler });
|
|
200
200
|
}
|
package/dist/pwa/server.js
CHANGED
|
@@ -283,7 +283,7 @@ import { registerProductsCrudRoutes } from './routes/products-crud.js';
|
|
|
283
283
|
// Products PUT update (#1013 Phase 93) — 1 endpoint (123 行)
|
|
284
284
|
import { registerProductsUpdateRoutes } from './routes/products-update.js';
|
|
285
285
|
// Products POST create (#1013 Phase 94) — 1 endpoint (232 行)
|
|
286
|
-
import { registerProductsCreateRoutes } from './routes/products-create.js';
|
|
286
|
+
import { registerProductsCreateRoutes, makeCreateProductHandler } from './routes/products-create.js';
|
|
287
287
|
// Products GET list (#1013 Phase 95) — 1 endpoint (399 行)
|
|
288
288
|
import { registerProductsListRoutes } from './routes/products-list.js';
|
|
289
289
|
// P2P 商品 (#1013 Phase 96) — 5 endpoints
|
|
@@ -4091,8 +4091,8 @@ app.use((req, res, next) => {
|
|
|
4091
4091
|
if (req.method === 'GET' && /^\/api\/(info|leaderboard|payment-methods|governance|openapi)/.test(req.path))
|
|
4092
4092
|
return next();
|
|
4093
4093
|
const apiKey = req.headers.authorization?.replace('Bearer ', '');
|
|
4094
|
-
if (!apiKey)
|
|
4095
|
-
return next(); // 无 key
|
|
4094
|
+
if (!apiKey || apiKey.startsWith('gtk_'))
|
|
4095
|
+
return next(); // 无 key→IP 速率;gtk_* 委托凭证(RFC-020)是独立自守 auth 通道(requireAgentGrantScope/resolveActiveGrantByBearer 管 safe-scope+active/expiry/revoked+audit),非 api_key,不进下面 agent 声明/风控/strike;否则合法 safe-scope grant 写(如 POST /api/agent-grants/permission-requests)被误判 AGENT_SCOPE_UNDECLARED。gtk_* 只命中 grant-aware 路由,别处 auth() 自然 401。
|
|
4096
4096
|
// 2026-05-23 P1 fix:被封禁用户仍可走以下路径(看自己 agent 状态 + 申诉)
|
|
4097
4097
|
// - GET /api/me/agents 系列(看强 / 申诉前必查)
|
|
4098
4098
|
// - POST /api/me/agents/strikes/:id/appeal(申诉权不能被封禁阻断)
|
|
@@ -7870,7 +7870,7 @@ registerWebauthnRoutes(app, {
|
|
|
7870
7870
|
origin: WEBAUTHN_ORIGIN,
|
|
7871
7871
|
challengeTtlMs: WEBAUTHN_CHALLENGE_TTL_MS,
|
|
7872
7872
|
gateTtlMs: WEBAUTHN_GATE_TTL_MS,
|
|
7873
|
-
invalidateAgentRiskCacheForUser,
|
|
7873
|
+
invalidateAgentRiskCacheForUser, createProductDraftHandler: makeCreateProductHandler({ db, auth, generateId, checkSellerCanList, getStakeDiscount, VALID_PRODUCT_TYPES, parsePlatformUrl, makeCommitmentHash, makeDescriptionHash, makePriceHash }), // RFC-020 PR-4: grant-gated warehouse draft reuses the human create logic
|
|
7874
7874
|
requireHumanPresence, // #1044 — DELETE passkey 自身需 token
|
|
7875
7875
|
});
|
|
7876
7876
|
// consumeGateToken / requireHumanPresence 已抽出到 ./human-presence.ts(PR-F0,behavior-zero,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seasonkoh/webaz",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "[PRE-LAUNCH] Agent-native decentralized commerce protocol. Humans and AI agents trade on the same protocol via MCP tools. ⚠️ Repository currently private until W8 public launch — GitHub links may return 404. See https://webaz.xyz for status.",
|
|
5
5
|
"main": "dist/mcp.js",
|
|
6
6
|
"bin": {
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
"test:agent-permission-requests": "tsx scripts/test-agent-permission-requests.ts",
|
|
89
89
|
"test:agent-permission-consume": "tsx scripts/test-agent-permission-consume.ts",
|
|
90
90
|
"test:agent-approvals-ui": "tsx scripts/test-agent-approvals-ui.ts",
|
|
91
|
+
"test:mcp-place-order-rail": "tsx scripts/test-mcp-place-order-rail.ts",
|
|
92
|
+
"test:agent-product-draft": "tsx scripts/test-agent-product-draft.ts",
|
|
91
93
|
"test:agent-grant-verify": "tsx scripts/test-agent-grant-verify.ts",
|
|
92
94
|
"test:agent-grant-mcp-consume": "tsx scripts/test-agent-grant-mcp-consume.ts",
|
|
93
95
|
"test:connected-agents-read": "tsx scripts/test-connected-agents-read.ts",
|