@agentxv2/gateway 0.1.1
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/.env.example +53 -0
- package/db/migrations/001_init.sql +117 -0
- package/db/migrations/002_agents.sql +20 -0
- package/db/migrations/003_a2a_results.sql +23 -0
- package/deploy/.env.deploy.example +23 -0
- package/deploy/add_swap_build.py +45 -0
- package/deploy/build_and_deploy.py +110 -0
- package/deploy/build_deploy_a2a.py +190 -0
- package/deploy/build_no_lint.py +52 -0
- package/deploy/build_on_fe.py +86 -0
- package/deploy/build_on_gw.py +98 -0
- package/deploy/build_prod.py +54 -0
- package/deploy/check_bal.py +31 -0
- package/deploy/check_code.py +47 -0
- package/deploy/check_deploy.sh +20 -0
- package/deploy/check_docs.py +16 -0
- package/deploy/check_frontend.py +42 -0
- package/deploy/check_frontend2.py +35 -0
- package/deploy/check_pks.py +34 -0
- package/deploy/check_status.py +32 -0
- package/deploy/check_v2.py +8 -0
- package/deploy/clean_install.py +28 -0
- package/deploy/deploy_a2a.py +58 -0
- package/deploy/deploy_a2a.sh +33 -0
- package/deploy/deploy_a2a_bg.sh +40 -0
- package/deploy/deploy_a2a_v2.py +151 -0
- package/deploy/deploy_a2a_v2.sh +36 -0
- package/deploy/deploy_bin.py +64 -0
- package/deploy/deploy_config.py +34 -0
- package/deploy/deploy_final.py +42 -0
- package/deploy/deploy_frontend.py +108 -0
- package/deploy/deploy_mcp.py +108 -0
- package/deploy/deploy_now.py +49 -0
- package/deploy/deploy_push_all.py +128 -0
- package/deploy/deploy_script.py +64 -0
- package/deploy/deploy_sep_a2a.sh +18 -0
- package/deploy/dev_mode.py +51 -0
- package/deploy/diag.py +12 -0
- package/deploy/diff_test.py +22 -0
- package/deploy/final_check.py +33 -0
- package/deploy/final_deploy.py +50 -0
- package/deploy/final_fix.py +54 -0
- package/deploy/final_push.py +114 -0
- package/deploy/find_frontend.py +38 -0
- package/deploy/fix_a2a.py +43 -0
- package/deploy/fix_and_build.py +77 -0
- package/deploy/fix_and_verify.py +56 -0
- package/deploy/fix_build.py +51 -0
- package/deploy/forge_deploy.py +67 -0
- package/deploy/full_push.py +101 -0
- package/deploy/full_push2.py +106 -0
- package/deploy/go_prod.py +59 -0
- package/deploy/gw_deploy.py +133 -0
- package/deploy/gw_final.py +206 -0
- package/deploy/gw_restart.py +28 -0
- package/deploy/gw_status.py +18 -0
- package/deploy/inline_build.py +99 -0
- package/deploy/one_shot.py +84 -0
- package/deploy/ox_deploy.py +32 -0
- package/deploy/pg_setup.py +36 -0
- package/deploy/prod_deploy.py +126 -0
- package/deploy/prod_final.py +140 -0
- package/deploy/prod_go.py +61 -0
- package/deploy/prod_one_shot.py +139 -0
- package/deploy/prod_setup.sh +62 -0
- package/deploy/prod_v2.py +91 -0
- package/deploy/prod_v3.py +103 -0
- package/deploy/push_docs.py +93 -0
- package/deploy/push_mcp.py +66 -0
- package/deploy/push_mcp_doc.py +28 -0
- package/deploy/push_v063.py +80 -0
- package/deploy/push_v2.py +71 -0
- package/deploy/qcheck.py +13 -0
- package/deploy/qcheck2.py +18 -0
- package/deploy/quick_check.py +8 -0
- package/deploy/rebuild.py +47 -0
- package/deploy/rebuild_frontend.py +44 -0
- package/deploy/restart.py +28 -0
- package/deploy/restore_frontend.py +42 -0
- package/deploy/restore_frontend2.py +41 -0
- package/deploy/restore_gateway.py +34 -0
- package/deploy/restore_quick.py +33 -0
- package/deploy/run_bash.py +19 -0
- package/deploy/run_sep.py +20 -0
- package/deploy/sdk_upgrade.py +50 -0
- package/deploy/server_prepare.py +41 -0
- package/deploy/server_setup.py +41 -0
- package/deploy/ssh_check.py +14 -0
- package/deploy/ssh_push.py +24 -0
- package/deploy/start_deploy.py +35 -0
- package/deploy/start_frontend.py +12 -0
- package/deploy/start_v2.py +29 -0
- package/deploy/stat.py +18 -0
- package/deploy/tail_log.py +11 -0
- package/deploy/update_env.py +32 -0
- package/deploy/upload_and_start.py +107 -0
- package/deploy/verify_a2a.py +13 -0
- package/deploy/verify_mcp.py +37 -0
- package/deploy/verify_sdk.py +21 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/crypto.d.ts +2 -0
- package/dist/lib/crypto.js +43 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/db.d.ts +3 -0
- package/dist/lib/db.js +26 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/ethers-utils.d.ts +60 -0
- package/dist/lib/ethers-utils.js +186 -0
- package/dist/lib/ethers-utils.js.map +1 -0
- package/dist/middleware/adminAuth.d.ts +2 -0
- package/dist/middleware/adminAuth.js +31 -0
- package/dist/middleware/adminAuth.js.map +1 -0
- package/dist/middleware/auth.d.ts +22 -0
- package/dist/middleware/auth.js +192 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +36 -0
- package/dist/middleware/error-handler.js +100 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +107 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/routes/a2a.d.ts +2 -0
- package/dist/routes/a2a.js +84 -0
- package/dist/routes/a2a.js.map +1 -0
- package/dist/routes/admin.d.ts +2 -0
- package/dist/routes/admin.js +180 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/agents.d.ts +2 -0
- package/dist/routes/agents.js +43 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/chat.d.ts +2 -0
- package/dist/routes/chat.js +183 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/history.d.ts +2 -0
- package/dist/routes/history.js +92 -0
- package/dist/routes/history.js.map +1 -0
- package/dist/routes/mcp.d.ts +13 -0
- package/dist/routes/mcp.js +513 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/tenant.d.ts +2 -0
- package/dist/routes/tenant.js +154 -0
- package/dist/routes/tenant.js.map +1 -0
- package/dist/services/a2a-worker.d.ts +7 -0
- package/dist/services/a2a-worker.js +406 -0
- package/dist/services/a2a-worker.js.map +1 -0
- package/dist/services/agent-indexer.d.ts +4 -0
- package/dist/services/agent-indexer.js +124 -0
- package/dist/services/agent-indexer.js.map +1 -0
- package/e2e_wallet.js +175 -0
- package/ecosystem.config.js +17 -0
- package/package.json +33 -0
- package/src/config.ts +47 -0
- package/src/index.ts +143 -0
- package/src/lib/crypto.ts +44 -0
- package/src/lib/db.ts +25 -0
- package/src/lib/ethers-utils.ts +229 -0
- package/src/middleware/adminAuth.ts +34 -0
- package/src/middleware/auth.ts +244 -0
- package/src/middleware/error-handler.ts +109 -0
- package/src/middleware/rate-limiter.ts +115 -0
- package/src/routes/a2a.ts +97 -0
- package/src/routes/admin.ts +219 -0
- package/src/routes/agents.ts +52 -0
- package/src/routes/chat.ts +219 -0
- package/src/routes/history.ts +114 -0
- package/src/routes/mcp.ts +567 -0
- package/src/routes/tenant.ts +196 -0
- package/src/services/a2a-worker.ts +486 -0
- package/src/services/agent-indexer.ts +127 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// AgentX Gateway — Auth Middleware
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// EIP-191 wallet signature auth + JWT + auto tenant creation.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.getChallenge = getChallenge;
|
|
12
|
+
exports.verifyChallenge = verifyChallenge;
|
|
13
|
+
exports.authMiddleware = authMiddleware;
|
|
14
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
const ethers_1 = require("ethers");
|
|
16
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
17
|
+
const config_1 = require("../config");
|
|
18
|
+
const db_1 = require("../lib/db");
|
|
19
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
20
|
+
// Challenge TTL in seconds (matches the 5-minute window)
|
|
21
|
+
const CHALLENGE_TTL_SEC = 5 * 60;
|
|
22
|
+
/**
|
|
23
|
+
* Store a challenge in Redis so it works across PM2 cluster workers.
|
|
24
|
+
* Falls back to an in-memory Map when Redis is unavailable (single-process dev).
|
|
25
|
+
*/
|
|
26
|
+
const fallbackMap = new Map();
|
|
27
|
+
function challengeKey(address) {
|
|
28
|
+
return `auth:challenge:${address}`;
|
|
29
|
+
}
|
|
30
|
+
async function saveChallenge(address, challenge) {
|
|
31
|
+
const r = (0, rate_limiter_1.getRedis)();
|
|
32
|
+
if (r) {
|
|
33
|
+
await r.setex(challengeKey(address), CHALLENGE_TTL_SEC, JSON.stringify(challenge));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fallbackMap.set(address, challenge);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function loadChallenge(address) {
|
|
40
|
+
const r = (0, rate_limiter_1.getRedis)();
|
|
41
|
+
if (r) {
|
|
42
|
+
const raw = await r.get(challengeKey(address));
|
|
43
|
+
if (raw)
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return fallbackMap.get(address);
|
|
48
|
+
}
|
|
49
|
+
async function deleteChallenge(address) {
|
|
50
|
+
const r = (0, rate_limiter_1.getRedis)();
|
|
51
|
+
if (r) {
|
|
52
|
+
await r.del(challengeKey(address));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
fallbackMap.delete(address);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function getChallenge(req, res) {
|
|
59
|
+
const address = (req.query.address || '').toLowerCase();
|
|
60
|
+
if (!address) {
|
|
61
|
+
res.status(400).json({ error: 'Missing wallet address' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
65
|
+
const nonce = crypto_1.default.randomBytes(16).toString('hex');
|
|
66
|
+
const message = `agentx:auth:${timestamp}:${nonce}`;
|
|
67
|
+
await saveChallenge(address, { address, timestamp, nonce });
|
|
68
|
+
res.json({ challenge: message, timestamp, nonce });
|
|
69
|
+
}
|
|
70
|
+
async function verifyChallenge(req, res) {
|
|
71
|
+
const { wallet_address, signature, timestamp, nonce } = req.body;
|
|
72
|
+
if (!wallet_address || !signature) {
|
|
73
|
+
res.status(400).json({ error: 'Missing wallet_address or signature' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const address = wallet_address.toLowerCase();
|
|
77
|
+
const challenge = await loadChallenge(address);
|
|
78
|
+
if (!challenge || challenge.nonce !== nonce) {
|
|
79
|
+
res.status(401).json({ error: 'Challenge expired or not found. Please request a new challenge.' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const expectedMessage = `agentx:auth:${timestamp}:${nonce}`;
|
|
83
|
+
let recovered;
|
|
84
|
+
try {
|
|
85
|
+
recovered = ethers_1.ethers.verifyMessage(expectedMessage, signature).toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (recovered !== address) {
|
|
92
|
+
res.status(401).json({ error: 'Signature does not match wallet address' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await deleteChallenge(address);
|
|
96
|
+
const pool = (0, db_1.getPool)();
|
|
97
|
+
let tenant = null;
|
|
98
|
+
const existing = await pool.query(`SELECT t.id, t.wallet_address, t.status,
|
|
99
|
+
t.quota_daily, t.quota_used, t.rate_limit_rpm, t.max_concurrent,
|
|
100
|
+
p.id as plan_id, p.slug as plan_slug
|
|
101
|
+
FROM tenants t
|
|
102
|
+
LEFT JOIN plans p ON t.plan_id = p.id
|
|
103
|
+
WHERE LOWER(t.wallet_address) = $1`, [address]);
|
|
104
|
+
if (existing.rows.length > 0) {
|
|
105
|
+
const row = existing.rows[0];
|
|
106
|
+
if (row.status === 'suspended') {
|
|
107
|
+
res.status(403).json({ error: 'Account suspended' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
tenant = {
|
|
111
|
+
id: row.id,
|
|
112
|
+
walletAddress: row.wallet_address,
|
|
113
|
+
planId: row.plan_id || '',
|
|
114
|
+
planSlug: row.plan_slug || 'free',
|
|
115
|
+
quotaDaily: row.quota_daily,
|
|
116
|
+
quotaUsed: row.quota_used,
|
|
117
|
+
rateLimitRpm: row.rate_limit_rpm,
|
|
118
|
+
maxConcurrent: row.max_concurrent,
|
|
119
|
+
status: row.status,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const freePlan = await pool.query(`SELECT id FROM plans WHERE slug = 'free' LIMIT 1`);
|
|
124
|
+
const planId = freePlan.rows[0]?.id || null;
|
|
125
|
+
const inserted = await pool.query(`INSERT INTO tenants (wallet_address, plan_id, quota_daily, rate_limit_rpm, max_concurrent)
|
|
126
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
127
|
+
RETURNING id`, [address, planId, 0, 5, 1]);
|
|
128
|
+
tenant = {
|
|
129
|
+
id: inserted.rows[0].id,
|
|
130
|
+
walletAddress: address,
|
|
131
|
+
planId: planId || '',
|
|
132
|
+
planSlug: 'free',
|
|
133
|
+
quotaDaily: 0,
|
|
134
|
+
quotaUsed: 0,
|
|
135
|
+
rateLimitRpm: 5,
|
|
136
|
+
maxConcurrent: 1,
|
|
137
|
+
status: 'active',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const token = jsonwebtoken_1.default.sign({ tenantId: tenant.id, walletAddress: tenant.walletAddress }, config_1.config.jwtSecret, { expiresIn: config_1.config.sessionTtlSec });
|
|
141
|
+
res.json({ access_token: token, expires_in: config_1.config.sessionTtlSec, tenant });
|
|
142
|
+
}
|
|
143
|
+
function authMiddleware(req, res, next) {
|
|
144
|
+
const authHeader = req.headers.authorization;
|
|
145
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
146
|
+
res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const token = authHeader.slice(7);
|
|
150
|
+
let decoded;
|
|
151
|
+
try {
|
|
152
|
+
decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
(0, db_1.getPool)()
|
|
159
|
+
.query(`SELECT t.id, t.wallet_address, t.status,
|
|
160
|
+
t.quota_daily, t.quota_used, t.rate_limit_rpm, t.max_concurrent,
|
|
161
|
+
p.id as plan_id, p.slug as plan_slug
|
|
162
|
+
FROM tenants t
|
|
163
|
+
LEFT JOIN plans p ON t.plan_id = p.id
|
|
164
|
+
WHERE t.id = $1`, [decoded.tenantId])
|
|
165
|
+
.then(r => {
|
|
166
|
+
if (r.rows.length === 0) {
|
|
167
|
+
res.status(401).json({ error: 'Tenant not found' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const row = r.rows[0];
|
|
171
|
+
if (row.status === 'suspended') {
|
|
172
|
+
res.status(403).json({ error: 'Account suspended' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
req.tenant = {
|
|
176
|
+
id: row.id,
|
|
177
|
+
walletAddress: row.wallet_address,
|
|
178
|
+
planId: row.plan_id || '',
|
|
179
|
+
planSlug: row.plan_slug || 'free',
|
|
180
|
+
quotaDaily: row.quota_daily,
|
|
181
|
+
quotaUsed: row.quota_used,
|
|
182
|
+
rateLimitRpm: row.rate_limit_rpm,
|
|
183
|
+
maxConcurrent: row.max_concurrent,
|
|
184
|
+
status: row.status,
|
|
185
|
+
};
|
|
186
|
+
next();
|
|
187
|
+
})
|
|
188
|
+
.catch(err => {
|
|
189
|
+
next(err);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export declare class AppError extends Error {
|
|
3
|
+
readonly statusCode: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly expose: boolean;
|
|
6
|
+
constructor(message: string, statusCode?: number, code?: string, expose?: boolean);
|
|
7
|
+
}
|
|
8
|
+
export declare class BadRequestError extends AppError {
|
|
9
|
+
constructor(message?: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class UnauthorizedError extends AppError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ForbiddenError extends AppError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class NotFoundError extends AppError {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class ConflictError extends AppError {
|
|
21
|
+
constructor(message?: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class TooManyRequestsError extends AppError {
|
|
24
|
+
constructor(message?: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Centralized Express error handler.
|
|
28
|
+
* - AppError subclasses → use their statusCode + expose message
|
|
29
|
+
* - Unknown errors → 500 with safe message (no leak)
|
|
30
|
+
*/
|
|
31
|
+
export declare function globalErrorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void;
|
|
32
|
+
/**
|
|
33
|
+
* Async route wrapper — catches thrown errors and forwards them to next().
|
|
34
|
+
* Usage: router.get('/path', asyncHandler(async (req, res) => { ... }))
|
|
35
|
+
*/
|
|
36
|
+
export declare function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// AgentX Gateway — Unified Error Handler
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// AppError hierarchy + global Express error middleware.
|
|
6
|
+
// Replaces scattered res.status(500).json({ error: err.message }) patterns.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TooManyRequestsError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = exports.AppError = void 0;
|
|
10
|
+
exports.globalErrorHandler = globalErrorHandler;
|
|
11
|
+
exports.asyncHandler = asyncHandler;
|
|
12
|
+
// ── Error Classes ────────────────────────────────────────────────────────
|
|
13
|
+
class AppError extends Error {
|
|
14
|
+
statusCode;
|
|
15
|
+
code;
|
|
16
|
+
expose;
|
|
17
|
+
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', expose = false) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.expose = expose;
|
|
22
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.AppError = AppError;
|
|
26
|
+
class BadRequestError extends AppError {
|
|
27
|
+
constructor(message = 'Bad request') {
|
|
28
|
+
super(message, 400, 'BAD_REQUEST', true);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.BadRequestError = BadRequestError;
|
|
32
|
+
class UnauthorizedError extends AppError {
|
|
33
|
+
constructor(message = 'Unauthorized') {
|
|
34
|
+
super(message, 401, 'UNAUTHORIZED', true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.UnauthorizedError = UnauthorizedError;
|
|
38
|
+
class ForbiddenError extends AppError {
|
|
39
|
+
constructor(message = 'Forbidden') {
|
|
40
|
+
super(message, 403, 'FORBIDDEN', true);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.ForbiddenError = ForbiddenError;
|
|
44
|
+
class NotFoundError extends AppError {
|
|
45
|
+
constructor(message = 'Not found') {
|
|
46
|
+
super(message, 404, 'NOT_FOUND', true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.NotFoundError = NotFoundError;
|
|
50
|
+
class ConflictError extends AppError {
|
|
51
|
+
constructor(message = 'Conflict') {
|
|
52
|
+
super(message, 409, 'CONFLICT', true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.ConflictError = ConflictError;
|
|
56
|
+
class TooManyRequestsError extends AppError {
|
|
57
|
+
constructor(message = 'Too many requests') {
|
|
58
|
+
super(message, 429, 'TOO_MANY_REQUESTS', true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.TooManyRequestsError = TooManyRequestsError;
|
|
62
|
+
// ── Global Error Handler Middleware ───────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Centralized Express error handler.
|
|
65
|
+
* - AppError subclasses → use their statusCode + expose message
|
|
66
|
+
* - Unknown errors → 500 with safe message (no leak)
|
|
67
|
+
*/
|
|
68
|
+
function globalErrorHandler(err, _req, res, _next) {
|
|
69
|
+
if (res.headersSent)
|
|
70
|
+
return;
|
|
71
|
+
if (err instanceof AppError) {
|
|
72
|
+
const body = {
|
|
73
|
+
error: err.expose ? err.message : 'Internal server error',
|
|
74
|
+
code: err.code,
|
|
75
|
+
};
|
|
76
|
+
// Attach details only in development
|
|
77
|
+
if (process.env.NODE_ENV === 'development' && !err.expose) {
|
|
78
|
+
body.detail = err.message;
|
|
79
|
+
}
|
|
80
|
+
res.status(err.statusCode).json(body);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Unknown / unexpected error — never leak message to client
|
|
84
|
+
console.error('[Gateway Error]', err);
|
|
85
|
+
res.status(500).json({
|
|
86
|
+
error: 'Internal server error',
|
|
87
|
+
code: 'INTERNAL_ERROR',
|
|
88
|
+
...(process.env.NODE_ENV === 'development' ? { detail: err.message } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Async route wrapper — catches thrown errors and forwards them to next().
|
|
93
|
+
* Usage: router.get('/path', asyncHandler(async (req, res) => { ... }))
|
|
94
|
+
*/
|
|
95
|
+
function asyncHandler(fn) {
|
|
96
|
+
return (req, res, next) => {
|
|
97
|
+
fn(req, res, next).catch(next);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,yCAAyC;AACzC,8EAA8E;AAC9E,wDAAwD;AACxD,4EAA4E;AAC5E,8EAA8E;;;AA+D9E,gDA4BC;AAMD,oCAMC;AAnGD,4EAA4E;AAE5E,MAAa,QAAS,SAAQ,KAAK;IACjB,UAAU,CAAQ;IAClB,IAAI,CAAQ;IACZ,MAAM,CAAS;IAE/B,YAAY,OAAe,EAAE,UAAU,GAAG,GAAG,EAAE,IAAI,GAAG,gBAAgB,EAAE,MAAM,GAAG,KAAK;QACpF,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAA;IACjD,CAAC;CACF;AAZD,4BAYC;AAED,MAAa,eAAgB,SAAQ,QAAQ;IAC3C,YAAY,OAAO,GAAG,aAAa;QACjC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,CAAC,CAAA;IAC1C,CAAC;CACF;AAJD,0CAIC;AAED,MAAa,iBAAkB,SAAQ,QAAQ;IAC7C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,CAAA;IAC3C,CAAC;CACF;AAJD,8CAIC;AAED,MAAa,cAAe,SAAQ,QAAQ;IAC1C,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,CAAA;IACxC,CAAC;CACF;AAJD,wCAIC;AAED,MAAa,aAAc,SAAQ,QAAQ;IACzC,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,CAAA;IACxC,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,aAAc,SAAQ,QAAQ;IACzC,YAAY,OAAO,GAAG,UAAU;QAC9B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAA;IACvC,CAAC;CACF;AAJD,sCAIC;AAED,MAAa,oBAAqB,SAAQ,QAAQ;IAChD,YAAY,OAAO,GAAG,mBAAmB;QACvC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,CAAC,CAAA;IAChD,CAAC;CACF;AAJD,oDAIC;AAED,6EAA6E;AAE7E;;;;GAIG;AACH,SAAgB,kBAAkB,CAChC,GAAU,EACV,IAAa,EACb,GAAa,EACb,KAAmB;IAEnB,IAAI,GAAG,CAAC,WAAW;QAAE,OAAM;IAE3B,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,GAA4B;YACpC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB;YACzD,IAAI,EAAE,GAAG,CAAC,IAAI;SACf,CAAA;QACD,qCAAqC;QACrC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YAC1D,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,OAAO,CAAA;QAC3B,CAAC;QACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrC,OAAM;IACR,CAAC;IAED,4DAA4D;IAC5D,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAA;IACrC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE,uBAAuB;QAC9B,IAAI,EAAE,gBAAgB;QACtB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACH,SAAgB,YAAY,CAC1B,EAAsE;IAEtE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAChC,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
export declare function getRedis(): Redis | null;
|
|
4
|
+
export declare function tenantRateLimiter(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
5
|
+
export declare function updateQuota(tenantId: string, tokens: number): Promise<void>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// AgentX Gateway — Rate Limiter Middleware
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Three-layer rate limiting:
|
|
6
|
+
// 1. Global IP-level (express-rate-limit)
|
|
7
|
+
// 2. Per-tenant RPM (Redis sliding window)
|
|
8
|
+
// 3. Per-tenant daily quota (Redis counter + DB fallback)
|
|
9
|
+
// 4. Per-tenant concurrency control (Redis)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.getRedis = getRedis;
|
|
16
|
+
exports.tenantRateLimiter = tenantRateLimiter;
|
|
17
|
+
exports.updateQuota = updateQuota;
|
|
18
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
19
|
+
const config_1 = require("../config");
|
|
20
|
+
const db_1 = require("../lib/db");
|
|
21
|
+
let redis = null;
|
|
22
|
+
function getRedis() {
|
|
23
|
+
if (!redis) {
|
|
24
|
+
redis = new ioredis_1.default(config_1.config.redisUrl, { lazyConnect: true });
|
|
25
|
+
redis.connect().catch(() => {
|
|
26
|
+
redis = null;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return redis;
|
|
30
|
+
}
|
|
31
|
+
async function tenantRateLimiter(req, res, next) {
|
|
32
|
+
const tenant = req.tenant;
|
|
33
|
+
if (!tenant) {
|
|
34
|
+
next();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const r = getRedis();
|
|
38
|
+
if (!r) {
|
|
39
|
+
next();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// 1. RPM check
|
|
44
|
+
const rpmKey = `rpm:${tenant.id}`;
|
|
45
|
+
const rpmCurrent = await r.incr(rpmKey);
|
|
46
|
+
if (rpmCurrent === 1) {
|
|
47
|
+
await r.expire(rpmKey, 60);
|
|
48
|
+
}
|
|
49
|
+
if (rpmCurrent > tenant.rateLimitRpm) {
|
|
50
|
+
res.status(429).json({
|
|
51
|
+
error: 'Rate limit exceeded',
|
|
52
|
+
retry_after: 60,
|
|
53
|
+
limit: tenant.rateLimitRpm,
|
|
54
|
+
limit_type: 'rpm',
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// 2. Daily quota check (only for platform mode)
|
|
59
|
+
if (tenant.quotaDaily > 0) {
|
|
60
|
+
const quotaKey = `quota:${tenant.id}`;
|
|
61
|
+
const used = await r.get(quotaKey);
|
|
62
|
+
const current = used ? parseInt(used, 10) : tenant.quotaUsed;
|
|
63
|
+
if (current >= tenant.quotaDaily) {
|
|
64
|
+
res.status(429).json({
|
|
65
|
+
error: 'Daily quota exceeded. Upgrade your plan or switch to BYOK mode.',
|
|
66
|
+
limit: tenant.quotaDaily,
|
|
67
|
+
used: current,
|
|
68
|
+
limit_type: 'daily_quota',
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 3. Concurrency check
|
|
74
|
+
const concurrencyKey = `concurrent:${tenant.id}`;
|
|
75
|
+
const currentConcurrent = await r.incr(concurrencyKey);
|
|
76
|
+
await r.expire(concurrencyKey, 300);
|
|
77
|
+
if (currentConcurrent > tenant.maxConcurrent) {
|
|
78
|
+
await r.decr(concurrencyKey);
|
|
79
|
+
res.status(429).json({
|
|
80
|
+
error: 'Too many concurrent requests',
|
|
81
|
+
limit: tenant.maxConcurrent,
|
|
82
|
+
limit_type: 'concurrency',
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
res.on('finish', () => {
|
|
87
|
+
r.decr(concurrencyKey).catch(() => { });
|
|
88
|
+
});
|
|
89
|
+
next();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
next();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function updateQuota(tenantId, tokens) {
|
|
96
|
+
const r = getRedis();
|
|
97
|
+
if (r) {
|
|
98
|
+
const quotaKey = `quota:${tenantId}`;
|
|
99
|
+
await r.incrby(quotaKey, tokens);
|
|
100
|
+
await r.expire(quotaKey, 86400);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const pool = (0, db_1.getPool)();
|
|
104
|
+
await pool.query(`UPDATE tenants SET quota_used = quota_used + $2 WHERE id = $1`, [tenantId, tokens]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// AgentX Gateway — A2A Task Results API
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// GET /api/v1/a2a/pending-tasks?agentId=X → tasks ready for agent to complete
|
|
6
|
+
// GET /api/v1/a2a/task-result/:taskId → single task LLM result
|
|
7
|
+
// GET /api/v1/a2a/worker-status → A2A worker health
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const express_1 = require("express");
|
|
11
|
+
const db_1 = require("../lib/db");
|
|
12
|
+
const a2a_worker_1 = require("../services/a2a-worker");
|
|
13
|
+
const router = (0, express_1.Router)();
|
|
14
|
+
// ── List pending task results for an agent ─────────────────────────────────
|
|
15
|
+
router.get('/pending-tasks', async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const agentId = parseInt(req.query.agentId);
|
|
18
|
+
if (!agentId || isNaN(agentId)) {
|
|
19
|
+
res.status(400).json({ error: 'agentId query parameter required' });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const pool = (0, db_1.getPool)();
|
|
23
|
+
const { rows } = await pool.query(`SELECT task_id, agent_id, task_type, input_data, output_data, status,
|
|
24
|
+
llm_model, tokens_used, processed_at, created_at
|
|
25
|
+
FROM a2a_task_results
|
|
26
|
+
WHERE agent_id = $1 AND status = 2
|
|
27
|
+
ORDER BY created_at DESC
|
|
28
|
+
LIMIT 50`, [agentId]);
|
|
29
|
+
res.json({ tasks: rows, total: rows.length, agentId });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error('[a2a] pending-tasks error:', err.message);
|
|
33
|
+
res.status(500).json({ error: 'Failed to fetch pending tasks' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// ── Single task result ─────────────────────────────────────────────────────
|
|
37
|
+
router.get('/task-result/:taskId', async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const taskId = parseInt(req.params.taskId);
|
|
40
|
+
if (!taskId || isNaN(taskId)) {
|
|
41
|
+
res.status(400).json({ error: 'Invalid taskId' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const pool = (0, db_1.getPool)();
|
|
45
|
+
const { rows } = await pool.query(`SELECT * FROM a2a_task_results WHERE task_id = $1`, [taskId]);
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
res.status(404).json({ error: 'Task result not found', taskId });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.json(rows[0]);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error('[a2a] task-result error:', err.message);
|
|
54
|
+
res.status(500).json({ error: 'Failed to fetch task result' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// ── Worker status ──────────────────────────────────────────────────────────
|
|
58
|
+
router.get('/worker-status', async (_req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const status = (0, a2a_worker_1.getWorkerStatus)();
|
|
61
|
+
const pool = (0, db_1.getPool)();
|
|
62
|
+
const { rows: stats } = await pool.query(`SELECT status, COUNT(*) as count
|
|
63
|
+
FROM a2a_task_results
|
|
64
|
+
GROUP BY status`);
|
|
65
|
+
const counts = { pending: 0, processing: 0, completed: 0, failed: 0 };
|
|
66
|
+
for (const r of stats) {
|
|
67
|
+
if (r.status === 0)
|
|
68
|
+
counts.pending = Number(r.count);
|
|
69
|
+
else if (r.status === 1)
|
|
70
|
+
counts.processing = Number(r.count);
|
|
71
|
+
else if (r.status === 2)
|
|
72
|
+
counts.completed = Number(r.count);
|
|
73
|
+
else if (r.status === 3)
|
|
74
|
+
counts.failed = Number(r.count);
|
|
75
|
+
}
|
|
76
|
+
res.json({ ...status, taskCounts: counts });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error('[a2a] worker-status error:', err.message);
|
|
80
|
+
res.status(500).json({ error: 'Failed to fetch worker status' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
exports.default = router;
|
|
84
|
+
//# sourceMappingURL=a2a.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a.js","sourceRoot":"","sources":["../../src/routes/a2a.ts"],"names":[],"mappings":"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"}
|