@seasonkoh/webaz 0.1.28 → 0.1.30
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/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -6,20 +6,29 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
6
6
|
const admin = requireContentAdmin(req, res);
|
|
7
7
|
if (!admin)
|
|
8
8
|
return;
|
|
9
|
+
// 审计项 G:补 payment_rail 列 + ?rail 过滤 —— admin/AML 此前在订单总览里区分不出 direct_p2p(非托管轨监控盲区)
|
|
9
10
|
const status = req.query.status;
|
|
11
|
+
const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
|
|
10
12
|
let sql = `SELECT o.id, o.product_id, o.buyer_id, o.seller_id, o.logistics_id, o.status,
|
|
11
|
-
o.total_amount, o.created_at,
|
|
13
|
+
o.total_amount, o.created_at, o.payment_rail,
|
|
12
14
|
p.title as product_title,
|
|
13
15
|
ub.name as buyer_name, us.name as seller_name
|
|
14
16
|
FROM orders o
|
|
15
17
|
JOIN products p ON o.product_id = p.id
|
|
16
18
|
JOIN users ub ON o.buyer_id = ub.id
|
|
17
19
|
JOIN users us ON o.seller_id = us.id`;
|
|
20
|
+
const where = [];
|
|
18
21
|
const params = [];
|
|
19
22
|
if (status && status.trim()) {
|
|
20
|
-
|
|
23
|
+
where.push('o.status = ?');
|
|
21
24
|
params.push(status);
|
|
22
25
|
}
|
|
26
|
+
if (rail) {
|
|
27
|
+
where.push('o.payment_rail = ?');
|
|
28
|
+
params.push(rail);
|
|
29
|
+
}
|
|
30
|
+
if (where.length)
|
|
31
|
+
sql += ' WHERE ' + where.join(' AND ');
|
|
23
32
|
sql += ` ORDER BY o.created_at DESC LIMIT 100`;
|
|
24
33
|
res.json({ orders: await dbAll(sql, params) });
|
|
25
34
|
});
|
|
@@ -27,20 +36,37 @@ export function registerAdminReportsRoutes(app, deps) {
|
|
|
27
36
|
const admin = requireArbitrationAdmin(req, res);
|
|
28
37
|
if (!admin)
|
|
29
38
|
return;
|
|
39
|
+
// 区分:按 status(open/in_review/resolved/dismissed)+ rail(direct_p2p/escrow)过滤;附 verdict/ruling_type/
|
|
40
|
+
// assigned_arbitrators/payment_rail 让 admin 分辨类型/进度/是否已指派/如何结案。summary 给全量分状态计数。
|
|
41
|
+
const status = typeof req.query.status === 'string' ? req.query.status.trim() : '';
|
|
42
|
+
const rail = typeof req.query.rail === 'string' ? req.query.rail.trim() : '';
|
|
43
|
+
const where = [];
|
|
44
|
+
const params = [];
|
|
45
|
+
if (status) {
|
|
46
|
+
where.push('d.status = ?');
|
|
47
|
+
params.push(status);
|
|
48
|
+
}
|
|
49
|
+
if (rail) {
|
|
50
|
+
where.push('o.payment_rail = ?');
|
|
51
|
+
params.push(rail);
|
|
52
|
+
}
|
|
30
53
|
const rows = await dbAll(`
|
|
31
54
|
SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status,
|
|
32
55
|
d.created_at, d.respond_deadline, d.arbitrate_deadline, d.resolved_at,
|
|
56
|
+
d.verdict, d.ruling_type, d.assigned_arbitrators,
|
|
33
57
|
u1.name as initiator_name, u2.name as defendant_name,
|
|
34
|
-
o.total_amount, o.status as order_status,
|
|
58
|
+
o.total_amount, o.status as order_status, o.payment_rail,
|
|
35
59
|
p.title as product_title
|
|
36
60
|
FROM disputes d
|
|
37
61
|
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
38
62
|
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
39
63
|
LEFT JOIN orders o ON d.order_id = o.id
|
|
40
64
|
LEFT JOIN products p ON o.product_id = p.id
|
|
65
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
41
66
|
ORDER BY d.created_at DESC LIMIT 100
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
`, params);
|
|
68
|
+
const counts = await dbAll('SELECT status, COUNT(*) n FROM disputes GROUP BY status');
|
|
69
|
+
res.json({ disputes: rows, counts: Object.fromEntries(counts.map(c => [c.status, c.n])), total: counts.reduce((s, c) => s + c.n, 0) });
|
|
44
70
|
});
|
|
45
71
|
app.get('/api/admin/verify-tasks', async (req, res) => {
|
|
46
72
|
const admin = requireArbitrationAdmin(req, res);
|
|
@@ -22,7 +22,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
22
22
|
}
|
|
23
23
|
const calls30d = (await dbOne(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`, [k.api_key])).n;
|
|
24
24
|
const last = await dbOne(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`, [k.api_key]);
|
|
25
|
-
const strikes = await dbAll(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]);
|
|
25
|
+
const strikes = await dbAll(`SELECT id, severity, reason_code, reason_detail, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]); // +id/detail:被封申诉 UI 需要(此端点封禁豁免)
|
|
26
26
|
let passport = null;
|
|
27
27
|
try {
|
|
28
28
|
passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
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';
|
|
3
|
+
import { initAgentDelegationGrantsSchema, initAgentPairingSchema, initAgentGrantAuthLogSchema, initAgentPermissionRequestsSchema } from '../../runtime/webaz-schema-helpers.js';
|
|
4
|
+
import { validateRequestedCapabilities, clampTtlSeconds, grantIsActive, resolveBundle, durationAllowedForScopes, suggestedDurationForScopes, durationToSeconds, riskLevelForScopes } from '../../runtime/agent-grant-scopes.js';
|
|
5
5
|
import { generateUserCode, verifyPkceS256, clampPairingTtlSeconds, pairingApprovable, pairingRetrievable } from '../../runtime/agent-pairing.js';
|
|
6
6
|
import { verifyGrantToken } from '../../runtime/agent-grant-verifier.js';
|
|
7
7
|
// Bounds on a pairing request (anti-bloat for the anonymous start endpoint).
|
|
8
8
|
const MAX_CAPABILITIES = 12;
|
|
9
9
|
const MAX_CONSTRAINTS_JSON = 2000;
|
|
10
|
+
// Thrown inside the approve transaction when the grant is no longer active (revoked/expired in the race
|
|
11
|
+
// window) — rolls the whole tx back so the request claim + expansion + audit are all-or-nothing.
|
|
12
|
+
class GrantInactiveError extends Error {
|
|
13
|
+
}
|
|
10
14
|
function safeParseCaps(json) {
|
|
11
15
|
try {
|
|
12
16
|
return JSON.parse(String(json));
|
|
@@ -28,11 +32,23 @@ function consentView(p) {
|
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
34
|
export function registerAgentGrantsRoutes(app, deps) {
|
|
31
|
-
const { db, auth, generateId, rateLimitOk } = deps;
|
|
35
|
+
const { db, auth, generateId, rateLimitOk, requireHumanPresence } = deps;
|
|
32
36
|
// PWA runtime self-init (MCP gets the tables via applyWebazRuntimeSchema). Idempotent.
|
|
33
37
|
initAgentDelegationGrantsSchema(db);
|
|
34
38
|
initAgentPairingSchema(db);
|
|
35
39
|
initAgentGrantAuthLogSchema(db);
|
|
40
|
+
initAgentPermissionRequestsSchema(db);
|
|
41
|
+
// Resolve the ACTIVE grant behind a gtk_ bearer (no scope check) — used to bind a permission request to
|
|
42
|
+
// (grant_id, human_id). Returns null on missing/expired/revoked. token_hash lookup mirrors the verifier.
|
|
43
|
+
async function resolveActiveGrantByBearer(req) {
|
|
44
|
+
const bearer = (req.header('authorization') || '').replace(/^Bearer\s+/i, '');
|
|
45
|
+
if (!bearer.startsWith('gtk_'))
|
|
46
|
+
return null;
|
|
47
|
+
const g = await dbOne('SELECT grant_id, human_id, agent_label, capabilities, status, expires_at, revoked_at FROM agent_delegation_grants WHERE token_hash = ?', [createHash('sha256').update(bearer).digest('hex')]);
|
|
48
|
+
if (!g || !grantIsActive(g, new Date().toISOString()))
|
|
49
|
+
return null;
|
|
50
|
+
return { grant_id: g.grant_id, human_id: g.human_id, agent_label: g.agent_label, capabilities: g.capabilities };
|
|
51
|
+
}
|
|
36
52
|
// ─────────────────────────── RFC-020 PR-C2a: opt-in grant-scope enforcement ───────────────────────────
|
|
37
53
|
// EXPLICIT, per-route, per-SAFE-scope. NOT global auth — a gtk_* token is accepted ONLY by routes that
|
|
38
54
|
// deliberately mount requireAgentGrantScope(scope); auth()/api_key is untouched and never accepts gtk_*.
|
|
@@ -60,8 +76,23 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
78
|
// Deny path: return the denial regardless of audit (no access is granted, so nothing to fail closed on).
|
|
63
|
-
if (!r.ok)
|
|
79
|
+
if (!r.ok) {
|
|
80
|
+
// Structured permission_required (RFC-020): the agent IS validly connected but its grant simply lacks
|
|
81
|
+
// this SAFE scope. Instead of a bare 403, hand it the exact next step — ask the human to expand the
|
|
82
|
+
// grant (approval_url + the create-request call), then retry this same request. Other grant failures
|
|
83
|
+
// (no/expired/revoked/suspended grant) stay plain: those aren't "request more", they must re-pair.
|
|
84
|
+
if (r.error_code === 'SCOPE_NOT_GRANTED') {
|
|
85
|
+
return void res.status(403).json({
|
|
86
|
+
error: `this action needs the "${scope}" permission, which your grant does not carry`,
|
|
87
|
+
error_code: 'PERMISSION_REQUIRED',
|
|
88
|
+
required_scope: scope,
|
|
89
|
+
approval_url: '/#agent-approvals',
|
|
90
|
+
request_permission: { method: 'POST', endpoint: '/api/agent-grants/permission-requests', body: { scopes: [scope] } },
|
|
91
|
+
note: 'Ask the human to approve at approval_url; on approval your existing grant is expanded — then retry this request.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
64
94
|
return void res.status(r.status).json({ error: r.error, error_code: r.error_code });
|
|
95
|
+
}
|
|
65
96
|
// Success path: FAIL CLOSED if the authorization could not be audited — a grant-authorized request
|
|
66
97
|
// must never proceed unaudited (RFC-020 invariant). Better to deny (503, retryable) than act unaccountably.
|
|
67
98
|
if (!audited)
|
|
@@ -75,6 +106,185 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
75
106
|
const p = req.agentGrant;
|
|
76
107
|
res.json({ grant: p, note: 'Authorized via delegation grant (safe scope read_public). This is a grant principal, not a human session.' });
|
|
77
108
|
});
|
|
109
|
+
// First REAL grant-consumed seller surface (Catalog Agent): read the grant human's OWN catalog. Read-only,
|
|
110
|
+
// money fields (commission/stake) excluded. A grant that lacks seller_products_read → structured
|
|
111
|
+
// permission_required (see requireAgentGrantScope) so the agent can request → human approves → retry.
|
|
112
|
+
// The consumption (allow AND the permission_required deny) is audited by the middleware.
|
|
113
|
+
app.get('/api/agent/seller/products', requireAgentGrantScope('seller_products_read'), async (req, res) => {
|
|
114
|
+
const p = req.agentGrant;
|
|
115
|
+
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
|
+
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
|
+
});
|
|
118
|
+
// helpers for the permission-request flow ----------------------------------------------------------------
|
|
119
|
+
const parseCapList = (json) => { try {
|
|
120
|
+
const a = JSON.parse(json);
|
|
121
|
+
return Array.isArray(a) ? a : [];
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
} };
|
|
126
|
+
const scopeNames = (json) => parseCapList(json).map(c => (typeof c === 'string' ? c : c?.capability)).filter((s) => typeof s === 'string');
|
|
127
|
+
// Returns true iff the audit row was durably written. Callers on the grant-authorized SUCCESS path MUST
|
|
128
|
+
// fail closed when this is false (RFC-020 invariant: a grant-authorized action is audited or it does not
|
|
129
|
+
// happen) — parity with the requireAgentGrantScope middleware above.
|
|
130
|
+
const auditGrant = async (grantId, humanId, cap, outcome, errorCode) => {
|
|
131
|
+
try {
|
|
132
|
+
await dbRun('INSERT INTO agent_grant_auth_log (grant_id, human_id, capability, outcome, error_code) VALUES (?,?,?,?,?)', [grantId, humanId, cap, outcome, errorCode ?? null]);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.error('[agent-grant] audit write failed:', e.message);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const bundleSummary = (key) => { const b = key ? resolveBundle(key) : null; return b ? b.human_summary : null; };
|
|
141
|
+
// GET verify — grant-authed. Returns the FULL grant (scopes, bundle, expiry, status), not just read_public.
|
|
142
|
+
// Audited (acceptance #8: every grant use logs). Never returns the raw token/api_key.
|
|
143
|
+
app.get('/api/agent-grants/verify', async (req, res) => {
|
|
144
|
+
const g = await resolveActiveGrantByBearer(req);
|
|
145
|
+
if (!g) {
|
|
146
|
+
return void res.status(401).json({ error: 'active delegation grant required', error_code: 'GRANT_REQUIRED' });
|
|
147
|
+
}
|
|
148
|
+
const full = await dbOne('SELECT grant_id, human_id, agent_label, capabilities, status, expires_at, permission_bundle FROM agent_delegation_grants WHERE grant_id = ?', [g.grant_id]);
|
|
149
|
+
// Fail closed: a grant-authorized read is audited or it does not proceed (parity with requireAgentGrantScope).
|
|
150
|
+
if (!(await auditGrant(g.grant_id, g.human_id, 'grant:verify', 'allow')))
|
|
151
|
+
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.' });
|
|
153
|
+
});
|
|
154
|
+
// POST create a permission request — the AGENT (holding its current grant) asks for MORE scope / a bundle.
|
|
155
|
+
// Bound to (human_id, grant_id) from the grant bearer. Rate-limited. Safe-only: risk/never-delegable are
|
|
156
|
+
// NOT grantable (they need a per-action live Passkey, not a persistent grant) → structured reject.
|
|
157
|
+
app.post('/api/agent-grants/permission-requests', async (req, res) => {
|
|
158
|
+
if (!rateLimitOk(`agent_perm_req:${req.ip || 'anon'}`, 20, 60_000))
|
|
159
|
+
return void res.status(429).json({ error: 'too_many_permission_requests', retry_after_s: 60 });
|
|
160
|
+
const g = await resolveActiveGrantByBearer(req);
|
|
161
|
+
if (!g)
|
|
162
|
+
return void res.status(401).json({ error: 'an active delegation grant is required to request more permissions (pair first with webaz_pair)', error_code: 'GRANT_REQUIRED' });
|
|
163
|
+
const body = (req.body || {});
|
|
164
|
+
const bundleKey = typeof body.bundle === 'string' ? body.bundle : null;
|
|
165
|
+
const bundle = bundleKey ? resolveBundle(bundleKey) : null;
|
|
166
|
+
if (bundleKey && !bundle)
|
|
167
|
+
return void res.status(400).json({ error: 'unknown permission bundle', error_code: 'UNKNOWN_BUNDLE' });
|
|
168
|
+
const scopes = bundle ? [...bundle.scopes] : (Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === 'string') : []);
|
|
169
|
+
if (scopes.length === 0)
|
|
170
|
+
return void res.status(400).json({ error: 'bundle or scopes required', error_code: 'NO_SCOPES' });
|
|
171
|
+
const v = validateRequestedCapabilities(scopes.map(s => ({ capability: s })));
|
|
172
|
+
if (!v.ok)
|
|
173
|
+
return void res.status(403).json({ error: 'permission_not_grantable', error_code: 'PERMISSION_NOT_GRANTABLE', rejected: v.rejected, note: 'Only safe (read/draft) scopes can be granted. Risk actions (order/publish/refund/…) require the human to act with a live Passkey — they are never delegated to a persistent grant.' });
|
|
174
|
+
const risk = riskLevelForScopes(scopes);
|
|
175
|
+
const duration = durationAllowedForScopes(scopes, body.duration) ? body.duration : suggestedDurationForScopes(scopes);
|
|
176
|
+
const id = generateId('apr');
|
|
177
|
+
const reqTtlIso = new Date(Date.now() + 7 * 86400_000).toISOString(); // request auto-expires in 7d if unanswered
|
|
178
|
+
await dbRun('INSERT INTO agent_permission_requests (id, human_id, grant_id, agent_label, requested_scopes, permission_bundle, reason, task_context, risk_level, duration, status, expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [id, g.human_id, g.grant_id, g.agent_label, JSON.stringify(scopes), bundleKey, typeof body.reason === 'string' ? body.reason.slice(0, 280) : null, typeof body.task_context === 'string' ? body.task_context.slice(0, 500) : null, risk, duration, 'pending', reqTtlIso]);
|
|
179
|
+
res.status(201).json({ approval_id: id, approval_url: '/#agent-approvals', status: 'pending', risk_level: risk, requested_scopes: scopes, permission_bundle: bundleKey, human_summary: bundleSummary(bundleKey), suggested_duration: duration, note: 'Ask the human to open approve_url (logged in) and approve. On approval your existing grant is expanded; then retry.' });
|
|
180
|
+
});
|
|
181
|
+
// GET list this human's PENDING permission requests (for #agent-approvals). Human-authed.
|
|
182
|
+
app.get('/api/agent-grants/permission-requests', async (req, res) => {
|
|
183
|
+
const user = auth(req, res);
|
|
184
|
+
if (!user)
|
|
185
|
+
return;
|
|
186
|
+
const rows = await dbAll("SELECT id, agent_label, requested_scopes, permission_bundle, reason, task_context, risk_level, duration, created_at, expires_at FROM agent_permission_requests WHERE human_id = ? AND status = 'pending' AND expires_at > ? ORDER BY created_at DESC LIMIT 100", [user.id, new Date().toISOString()]);
|
|
187
|
+
res.json({ requests: rows.map(r => ({ ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) })) });
|
|
188
|
+
});
|
|
189
|
+
// GET list the requests THIS grant created — GRANT-authed (the agent, via webaz_pair), so an agent can poll
|
|
190
|
+
// its own request status (pending/approved/rejected/expired) without hitting the target surface. Bound to
|
|
191
|
+
// grant_id: an agent sees ONLY its own requests, never the human's other agents'. Audited (fail-closed).
|
|
192
|
+
app.get('/api/agent-grants/my-permission-requests', async (req, res) => {
|
|
193
|
+
if (!rateLimitOk(`agent_perm_list:${req.ip || 'anon'}`, 30, 60_000))
|
|
194
|
+
return void res.status(429).json({ error: 'too_many_requests', error_code: 'GRANT_RATE_LIMITED', retry_after_s: 60 });
|
|
195
|
+
const g = await resolveActiveGrantByBearer(req);
|
|
196
|
+
if (!g)
|
|
197
|
+
return void res.status(401).json({ error: 'an active delegation grant is required (pair first with webaz_pair)', error_code: 'GRANT_REQUIRED' });
|
|
198
|
+
const rows = await dbAll('SELECT id, requested_scopes, permission_bundle, risk_level, duration, status, created_at, expires_at, approved_at FROM agent_permission_requests WHERE grant_id = ? ORDER BY created_at DESC LIMIT 50', [g.grant_id]);
|
|
199
|
+
if (!(await auditGrant(g.grant_id, g.human_id, 'grant:list_requests', 'allow')))
|
|
200
|
+
return void res.status(503).json({ error: 'authorization audit unavailable; refusing to proceed unaudited', error_code: 'GRANT_AUDIT_FAILED' });
|
|
201
|
+
res.json({ requests: rows.map(r => ({ ...r, requested_scopes: scopeNames(String(r.requested_scopes)), human_summary: bundleSummary(r.permission_bundle) })) });
|
|
202
|
+
});
|
|
203
|
+
// RFC-020: expanding an agent grant is a privilege escalation (like initial pairing) — a stolen web session
|
|
204
|
+
// must NOT widen an agent from read_public to a long-term bundle. So a LIVE Passkey bound to this request_id
|
|
205
|
+
// is required. Grant-active is checked BEFORE claiming the request, and the expand is guarded + reversible,
|
|
206
|
+
// so a mid-flight expire/revoke can never strand a phantom 'approved'.
|
|
207
|
+
// POST approve — human-authed + live Passkey; expands the bound grant (union scopes + bundle + extend expiry). Audited.
|
|
208
|
+
app.post('/api/agent-grants/permission-requests/:id/approve', async (req, res) => {
|
|
209
|
+
const user = auth(req, res);
|
|
210
|
+
if (!user)
|
|
211
|
+
return;
|
|
212
|
+
const now = new Date().toISOString();
|
|
213
|
+
const r = await dbOne('SELECT human_id, grant_id, requested_scopes, permission_bundle, duration, status, expires_at FROM agent_permission_requests WHERE id = ?', [req.params.id]);
|
|
214
|
+
if (!r)
|
|
215
|
+
return void res.status(404).json({ error: 'permission_request_not_found' });
|
|
216
|
+
if (r.human_id !== user.id)
|
|
217
|
+
return void res.status(403).json({ error: 'not your permission request' });
|
|
218
|
+
if (r.status !== 'pending' || r.expires_at <= now)
|
|
219
|
+
return void res.status(409).json({ error: 'permission_request_not_pending', status: r.status });
|
|
220
|
+
// Live Passkey, bound to THIS request (a token minted for request A can't approve B).
|
|
221
|
+
const hp = requireHumanPresence(user.id, 'agent_permission_approve', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_permission_approve', (data) => { try {
|
|
222
|
+
return typeof data === 'object' && data !== null && data.request_id === req.params.id;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
} });
|
|
227
|
+
if (!hp.ok)
|
|
228
|
+
return void res.status(412).json({ error: hp.reason, error_code: hp.error_code });
|
|
229
|
+
const reqScopes = scopeNames(r.requested_scopes);
|
|
230
|
+
// Defense in depth: re-validate safe-only + duration allowed at approval time.
|
|
231
|
+
if (!validateRequestedCapabilities(reqScopes.map(s => ({ capability: s }))).ok)
|
|
232
|
+
return void res.status(403).json({ error: 'permission_not_grantable', error_code: 'PERMISSION_NOT_GRANTABLE' });
|
|
233
|
+
if (!durationAllowedForScopes(reqScopes, r.duration))
|
|
234
|
+
return void res.status(403).json({ error: 'duration_not_allowed_for_risk', error_code: 'DURATION_NOT_ALLOWED' });
|
|
235
|
+
// (P2) Verify the grant is ACTIVE *before* claiming the request — never leave a phantom 'approved'.
|
|
236
|
+
const grant = await dbOne('SELECT grant_id, capabilities, status, expires_at, revoked_at FROM agent_delegation_grants WHERE grant_id = ?', [r.grant_id]);
|
|
237
|
+
if (!grant || !grantIsActive(grant, now))
|
|
238
|
+
return void res.status(409).json({ error: 'grant_inactive', error_code: 'GRANT_INACTIVE', note: 'the agent grant expired or was revoked; re-pair (this request stays pending)' });
|
|
239
|
+
// Union new scopes into the grant; set bundle; extend expiry (never shorten). 'once' → short 1h window.
|
|
240
|
+
const union = [...new Set([...scopeNames(grant.capabilities), ...reqScopes])].map(s => ({ capability: s, constraints: {} }));
|
|
241
|
+
const secs = durationToSeconds(r.duration) || 3600;
|
|
242
|
+
const newExpiry = new Date(Date.now() + secs * 1000).toISOString();
|
|
243
|
+
const expiresAt = newExpiry > grant.expires_at ? newExpiry : grant.expires_at;
|
|
244
|
+
// ATOMIC (RFC-020 invariant: a grant expansion is audited-or-it-does-not-happen; parity with arbitrator
|
|
245
|
+
// approve). CAS-claim the request + guarded-expand the grant + write the audit row in ONE sync
|
|
246
|
+
// db.transaction. If the audit INSERT throws, OR the grant was revoked in the race window (guarded
|
|
247
|
+
// WHERE → 0 rows → GrantInactiveError), the WHOLE tx rolls back: scopes unchanged, request stays pending.
|
|
248
|
+
let outcome;
|
|
249
|
+
try {
|
|
250
|
+
outcome = db.transaction(() => {
|
|
251
|
+
const claimed = db.prepare("UPDATE agent_permission_requests SET status='approved', approved_at=? WHERE id=? AND status='pending'").run(now, req.params.id);
|
|
252
|
+
if (claimed.changes !== 1)
|
|
253
|
+
return 'not_pending';
|
|
254
|
+
const expanded = db.prepare("UPDATE agent_delegation_grants SET capabilities=?, permission_bundle=COALESCE(?, permission_bundle), expires_at=? WHERE grant_id=? AND status='active' AND revoked_at IS NULL").run(JSON.stringify(union), r.permission_bundle, expiresAt, grant.grant_id);
|
|
255
|
+
if (expanded.changes !== 1)
|
|
256
|
+
throw new GrantInactiveError();
|
|
257
|
+
db.prepare('INSERT INTO agent_grant_auth_log (grant_id, human_id, capability, outcome, error_code) VALUES (?,?,?,?,?)').run(grant.grant_id, user.id, `permission_request:approve:${r.permission_bundle || reqScopes.join(',')}`, 'allow', null);
|
|
258
|
+
return 'expanded';
|
|
259
|
+
})();
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
if (e instanceof GrantInactiveError)
|
|
263
|
+
return void res.status(409).json({ error: 'grant_inactive', error_code: 'GRANT_INACTIVE', note: 'the agent grant was revoked while approving; request stays pending' });
|
|
264
|
+
console.error('[agent-grant] approve tx failed (audit unavailable?):', e.message);
|
|
265
|
+
return void res.status(503).json({ error: 'authorization audit unavailable; refusing to expand unaudited', error_code: 'GRANT_AUDIT_FAILED' });
|
|
266
|
+
}
|
|
267
|
+
if (outcome === 'not_pending')
|
|
268
|
+
return void res.status(409).json({ error: 'permission_request_not_pending' });
|
|
269
|
+
res.json({ success: true, grant_id: grant.grant_id, scopes: union.map(u => u.capability), permission_bundle: r.permission_bundle, expires_at: expiresAt });
|
|
270
|
+
});
|
|
271
|
+
// POST reject — human-authed. Terminal 'rejected'; nothing is granted.
|
|
272
|
+
app.post('/api/agent-grants/permission-requests/:id/reject', async (req, res) => {
|
|
273
|
+
const user = auth(req, res);
|
|
274
|
+
if (!user)
|
|
275
|
+
return;
|
|
276
|
+
const r = await dbOne('SELECT human_id, status FROM agent_permission_requests WHERE id = ?', [req.params.id]);
|
|
277
|
+
if (!r)
|
|
278
|
+
return void res.status(404).json({ error: 'permission_request_not_found' });
|
|
279
|
+
if (r.human_id !== user.id)
|
|
280
|
+
return void res.status(403).json({ error: 'not your permission request' });
|
|
281
|
+
if (r.status !== 'pending')
|
|
282
|
+
return void res.status(409).json({ error: 'permission_request_not_pending', status: r.status });
|
|
283
|
+
const rj = await dbRun("UPDATE agent_permission_requests SET status='rejected' WHERE id=? AND status='pending'", [req.params.id]);
|
|
284
|
+
if (!rj || rj.changes !== 1)
|
|
285
|
+
return void res.status(409).json({ error: 'permission_request_not_pending' });
|
|
286
|
+
res.json({ success: true, status: 'rejected' });
|
|
287
|
+
});
|
|
78
288
|
// ─────────────────────────── RFC-020 PR-C1: pairing (device-flow + PKCE) ───────────────────────────
|
|
79
289
|
// C1 = pairing + credential delivery ONLY. No grant is consumed by any tool here (that is PR-C2).
|
|
80
290
|
// (pair 1) Agent starts a pairing — UNAUTHENTICATED (agent has no credential yet). Safe scopes only.
|
|
@@ -135,6 +345,17 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
135
345
|
return void res.status(404).json({ error: 'pairing_not_found' });
|
|
136
346
|
if (!pairingApprovable(p, now))
|
|
137
347
|
return void res.status(409).json({ error: 'pairing_not_pending_or_expired', status: p.status });
|
|
348
|
+
// 人工在场 gate:批准 = 真人 Passkey/WebAuthn 确认(默认必需,param 可关)。挡"账号被静默批准"。
|
|
349
|
+
// 注:这挡的是"是不是真人在批",不挡 illicit-consent(人被骗批错 agent)—— 那由前端强制口令核对 + safe-scope 兜底。
|
|
350
|
+
// token 必须【绑定这个配对码】(purpose_data.user_code === :user_code)—— 防"为批 A 拿到的 token 被首次提交去批 B"(与 delete_passkey 绑 credential_id 同法)。
|
|
351
|
+
const hp = requireHumanPresence(user.id, 'agent_pair_approve', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_pair_approve', (data) => { try {
|
|
352
|
+
return typeof data === 'object' && data !== null && data.user_code === req.params.user_code;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false;
|
|
356
|
+
} });
|
|
357
|
+
if (!hp.ok)
|
|
358
|
+
return void res.status(412).json({ error: hp.reason, error_code: hp.error_code });
|
|
138
359
|
// Re-validate scopes at approval time (defense in depth) — must still be safe-only.
|
|
139
360
|
const caps = safeParseCaps(p.capabilities);
|
|
140
361
|
const v = validateRequestedCapabilities(caps);
|
|
@@ -142,11 +363,31 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
142
363
|
return void res.status(403).json({ error: 'pairing_rejected', rejected: v.rejected });
|
|
143
364
|
const grantId = generateId('grt');
|
|
144
365
|
const expiresAt = new Date(Date.now() + clampTtlSeconds(undefined) * 1000).toISOString();
|
|
366
|
+
// 先【CAS 抢占】pending 配对(唯一赢家),再插 grant —— 竞态下输家 changes!==1 直接 409、不插任何 grant,
|
|
367
|
+
// 杜绝"插了 grant 但配对更新失败"留下 token_hash NULL 的 orphan grant(污染连接记录)。
|
|
368
|
+
const claimed = 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]);
|
|
369
|
+
if (!claimed || claimed.changes !== 1)
|
|
370
|
+
return void res.status(409).json({ error: 'pairing_not_pending_or_expired' });
|
|
145
371
|
// Grant created WITHOUT a token (token_hash NULL) — the bearer is minted only at retrieval.
|
|
146
372
|
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
373
|
res.json({ success: true, grant_id: grantId, capabilities: caps });
|
|
149
374
|
});
|
|
375
|
+
// (pair 3b) Human rejects — human-authenticated. Terminal 'rejected' → agent's retrieve fails clearly (no silent lingering).
|
|
376
|
+
// 拒绝是保护性动作,无需 Passkey(不签发任何凭证)。幂等:仅 pending 可拒。
|
|
377
|
+
app.post('/api/agent-grants/pair/:user_code/reject', async (req, res) => {
|
|
378
|
+
const user = auth(req, res);
|
|
379
|
+
if (!user)
|
|
380
|
+
return;
|
|
381
|
+
const p = await dbOne('SELECT status FROM agent_pairing_sessions WHERE user_code = ?', [req.params.user_code]);
|
|
382
|
+
if (!p)
|
|
383
|
+
return void res.status(404).json({ error: 'pairing_not_found' });
|
|
384
|
+
if (p.status !== 'pending')
|
|
385
|
+
return void res.status(409).json({ error: 'pairing_not_pending', status: p.status });
|
|
386
|
+
const r = await dbRun("UPDATE agent_pairing_sessions SET status='rejected', human_id=? WHERE user_code=? AND status='pending'", [user.id, req.params.user_code]);
|
|
387
|
+
if (!r || r.changes !== 1)
|
|
388
|
+
return void res.status(409).json({ error: 'pairing_not_pending' });
|
|
389
|
+
res.json({ success: true, status: 'rejected' });
|
|
390
|
+
});
|
|
150
391
|
// (pair 4) Agent retrieves the credential ONCE via PKCE verifier — UNAUTHENTICATED (PKCE-gated).
|
|
151
392
|
app.post('/api/agent-grants/pair/:pairing_id/retrieve', async (req, res) => {
|
|
152
393
|
const now = new Date().toISOString();
|
|
@@ -180,36 +421,16 @@ export function registerAgentGrantsRoutes(app, deps) {
|
|
|
180
421
|
expires_at: grant.expires_at,
|
|
181
422
|
});
|
|
182
423
|
});
|
|
183
|
-
// ──
|
|
184
|
-
|
|
424
|
+
// ── Direct grant issuance is DISABLED — single blessed path is the Passkey pairing flow. ──
|
|
425
|
+
// 旧的"仅登录即直接 mint 原文 bearer"入口会旁路 #pair 的真人 Passkey 批准,削弱"human Passkey-approves
|
|
426
|
+
// agent delegation"的安全叙事。零消费方(前端/MCP/测试均不用),故降级为不可用,统一走 pairing。
|
|
427
|
+
app.post('/api/agent-grants', (req, res) => {
|
|
185
428
|
const user = auth(req, res);
|
|
186
429
|
if (!user)
|
|
187
430
|
return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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).',
|
|
431
|
+
return void res.status(410).json({
|
|
432
|
+
error: 'USE_PAIRING_FLOW', error_code: 'USE_PAIRING_FLOW',
|
|
433
|
+
note: 'Direct grant issuance is disabled. Start pairing with webaz_pair (action=start); the human approves at /#pair with a Passkey, then the agent retrieves the credential with its PKCE verifier. No endpoint mints a bearer without a live Passkey ceremony bound to the pairing.',
|
|
213
434
|
});
|
|
214
435
|
});
|
|
215
436
|
// ── Read: the human's connected agents (no secrets) + recent-use from the audit log (PR-D). ──
|
|
@@ -141,6 +141,8 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
141
141
|
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_orders,
|
|
142
142
|
SUM(CASE WHEN status IN ('paid','accepted','shipped','picked_up','in_transit','delivered','confirmed') THEN 1 ELSE 0 END) as in_progress_orders,
|
|
143
143
|
COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv,
|
|
144
|
+
COALESCE(SUM(CASE WHEN status = 'completed' AND COALESCE(payment_rail,'escrow') = 'escrow' THEN total_amount ELSE 0 END), 0) as gmv_escrow,
|
|
145
|
+
COALESCE(SUM(CASE WHEN status = 'completed' AND payment_rail = 'direct_p2p' THEN total_amount ELSE 0 END), 0) as gmv_direct_pay,
|
|
144
146
|
COALESCE(AVG(CASE WHEN status = 'completed' THEN total_amount END), 0) as aov
|
|
145
147
|
FROM orders WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
146
148
|
`, [user.id, windowDays]));
|
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { grantArbitrator, grantArbitratorTx, suspendArbitrator, reinstateArbitrator, revokeArbitrator, listArbitrators } from '../arbitrator-lifecycle.js'; // PR-B/PR-C.2 生产仲裁员生命周期域逻辑
|
|
2
3
|
export function registerArbitratorRoutes(app, deps) {
|
|
3
4
|
// 只读/单写站点走 RFC-016 异步 seam;db 保留:apply/withdraw/approve/reject 是
|
|
4
5
|
// stake 资金路径,状态翻转 + 钱包扣/退必须原子(db.transaction + CAS),Phase 3 迁 pg 行锁。
|
|
5
|
-
const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
|
|
6
|
+
const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, consumeGateToken, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
|
|
7
|
+
// ── PR-B:生产仲裁员生命周期(grant/suspend/reinstate/revoke)。门(四端点共用):ROOT/admin auth +
|
|
8
|
+
// 现场真人 Passkey(purpose 绑动作 + purpose_data.user_id 绑目标,杜绝跨动作/跨目标复用)+ admin_audit_log(含失败留痕)。
|
|
9
|
+
// 域逻辑(active-only 授权源 / revoked 终态 / 拒非人类)在 arbitrator-lifecycle.ts;此处只做 auth + Passkey + 审计。
|
|
10
|
+
const arbLifecycle = (action, purpose, fn) => (req, res) => {
|
|
11
|
+
const admin = requireArbitrationAdmin(req, res);
|
|
12
|
+
if (!admin)
|
|
13
|
+
return;
|
|
14
|
+
const userId = String((action === 'grant' ? req.body?.user_id : req.params.user_id) || '');
|
|
15
|
+
const note = (req.body?.note ?? null);
|
|
16
|
+
if (!userId)
|
|
17
|
+
return void errorRes(res, 400, 'MISSING_USER_ID', '缺少目标 user_id');
|
|
18
|
+
const gate = consumeGateToken(admin.id, req.body?.webauthn_token, purpose, (d) => d?.user_id === userId);
|
|
19
|
+
if (!gate.ok) {
|
|
20
|
+
logAdminAction(admin.id, `arbitrator.${action}`, 'user', userId, { ok: false, gate: gate.reason }); // ROOT 尝试也留痕(purpose 不符/无 token)
|
|
21
|
+
return void errorRes(res, 412, 'HUMAN_PRESENCE_REQUIRED', gate.reason || '此操作需现场真人 Passkey 确认');
|
|
22
|
+
}
|
|
23
|
+
const r = fn(userId, admin.id, note);
|
|
24
|
+
logAdminAction(admin.id, `arbitrator.${action}`, 'user', userId, { ok: r.ok, error_code: r.error_code, note });
|
|
25
|
+
if (!r.ok)
|
|
26
|
+
return void errorRes(res, 409, r.error_code || 'ARB_MUTATION_FAILED', r.error || '操作失败');
|
|
27
|
+
res.json({ success: true, user_id: userId, action });
|
|
28
|
+
};
|
|
29
|
+
app.post('/api/admin/arbitrators/grant', arbLifecycle('grant', 'arbitrator_grant', (userId, adminId, note) => grantArbitrator(db, { userId, grantedBy: adminId, note })));
|
|
30
|
+
app.post('/api/admin/arbitrators/:user_id/suspend', arbLifecycle('suspend', 'arbitrator_suspend', (userId, _a, note) => suspendArbitrator(db, { userId, note })));
|
|
31
|
+
app.post('/api/admin/arbitrators/:user_id/reinstate', arbLifecycle('reinstate', 'arbitrator_reinstate', (userId, _a, note) => reinstateArbitrator(db, { userId, note })));
|
|
32
|
+
app.post('/api/admin/arbitrators/:user_id/revoke', arbLifecycle('revoke', 'arbitrator_revoke', (userId, _a, note) => revokeArbitrator(db, { userId, note })));
|
|
33
|
+
// 名册(admin 只读,无需 Passkey)。含 active/suspended/revoked 全量 + 状态。
|
|
34
|
+
app.get('/api/admin/arbitrators', (req, res) => {
|
|
35
|
+
const admin = requireArbitrationAdmin(req, res);
|
|
36
|
+
if (!admin)
|
|
37
|
+
return;
|
|
38
|
+
res.json({ arbitrators: listArbitrators(db) });
|
|
39
|
+
});
|
|
6
40
|
app.get('/api/arbitrator/eligibility', (req, res) => {
|
|
7
41
|
const user = auth(req, res);
|
|
8
42
|
if (!user)
|
|
@@ -123,30 +157,49 @@ export function registerArbitratorRoutes(app, deps) {
|
|
|
123
157
|
const admin = requireArbitrationAdmin(req, res);
|
|
124
158
|
if (!admin)
|
|
125
159
|
return;
|
|
126
|
-
const
|
|
160
|
+
const note = (req.body?.note ?? null);
|
|
127
161
|
const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
|
|
128
162
|
if (!appRow)
|
|
129
163
|
return void res.json({ error: '申请不存在' });
|
|
130
164
|
if (appRow.status !== 'pending')
|
|
131
165
|
return void res.json({ error: '该申请不在待审状态' });
|
|
132
|
-
//
|
|
166
|
+
// PR-B:批准 = 现场真人 Passkey(purpose 绑目标 user_id)+ 走【统一】grantArbitrator(含 revoked-terminal +
|
|
167
|
+
// 拒 system/agent/无 Passkey)。绝不 INSERT OR REPLACE 白名单(那会复活被撤销用户)。审计成功与失败。
|
|
168
|
+
const gate = consumeGateToken(admin.id, req.body?.webauthn_token, 'arbitrator_grant', (d) => d?.user_id === appRow.user_id);
|
|
169
|
+
if (!gate.ok) {
|
|
170
|
+
logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, gate: gate.reason });
|
|
171
|
+
return void errorRes(res, 412, 'HUMAN_PRESENCE_REQUIRED', gate.reason || '此操作需现场真人 Passkey 确认');
|
|
172
|
+
}
|
|
173
|
+
// PR-C.2 原子:grant 白名单 + 翻转申请状态在【同一事务】。任一失败一起回滚 —— 杜绝"grant 成功但申请已被并发撤回/拒绝"。
|
|
174
|
+
let gErr = null;
|
|
175
|
+
let raced = false;
|
|
133
176
|
try {
|
|
134
177
|
db.transaction(() => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
throw new Error('
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
const g = grantArbitratorTx(db, { userId: appRow.user_id, grantedBy: admin.id, note: note ?? '外部申请批准' });
|
|
179
|
+
if (!g.ok) {
|
|
180
|
+
gErr = g.error_code || 'GRANT_FAILED';
|
|
181
|
+
throw new Error('ARB_ABORT');
|
|
182
|
+
} // 回滚(此时未写 CAS)
|
|
183
|
+
const cas = db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'").run(admin.id, note, appRow.id);
|
|
184
|
+
if (cas.changes === 0) {
|
|
185
|
+
raced = true;
|
|
186
|
+
throw new Error('ARB_ABORT');
|
|
187
|
+
} // 并发已改状态 → 回滚 grant
|
|
141
188
|
})();
|
|
142
189
|
}
|
|
143
190
|
catch (e) {
|
|
144
|
-
if (e.message
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
191
|
+
if (e.message !== 'ARB_ABORT')
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
if (raced) {
|
|
195
|
+
logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, raced: true });
|
|
196
|
+
return void res.json({ error: '该申请不在待审状态' });
|
|
197
|
+
}
|
|
198
|
+
if (gErr) {
|
|
199
|
+
logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: false, error_code: gErr });
|
|
200
|
+
return void errorRes(res, 409, gErr, '批准失败');
|
|
148
201
|
}
|
|
149
|
-
logAdminAction(admin.id, '
|
|
202
|
+
logAdminAction(admin.id, 'arbitrator.approve', 'user', appRow.user_id, { ok: true });
|
|
150
203
|
res.json({ success: true });
|
|
151
204
|
});
|
|
152
205
|
app.post('/api/admin/arbitrator-applications/:id/reject', async (req, res) => {
|