@radaros/transport 0.3.46 → 0.3.49
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/index.cjs +243 -7
- package/dist/index.d.cts +49 -7
- package/dist/index.d.ts +49 -7
- package/dist/index.js +240 -7
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,9 @@ __export(index_exports, {
|
|
|
28
28
|
createAgentRouter: () => createAgentRouter,
|
|
29
29
|
createBrowserGateway: () => createBrowserGateway,
|
|
30
30
|
createFileUploadMiddleware: () => createFileUploadMiddleware,
|
|
31
|
+
createGatewayRouter: () => createGatewayRouter,
|
|
32
|
+
createJwtMiddleware: () => createJwtMiddleware,
|
|
33
|
+
createRbacMiddleware: () => createRbacMiddleware,
|
|
31
34
|
createVisionGateway: () => createVisionGateway,
|
|
32
35
|
createVoiceGateway: () => createVoiceGateway,
|
|
33
36
|
errorHandler: () => errorHandler,
|
|
@@ -722,6 +725,165 @@ function buildMultiModalInput(body, files) {
|
|
|
722
725
|
return parts;
|
|
723
726
|
}
|
|
724
727
|
|
|
728
|
+
// src/express/gateway.ts
|
|
729
|
+
var import_node_module4 = require("module");
|
|
730
|
+
var _require4 = (0, import_node_module4.createRequire)(importMetaUrl);
|
|
731
|
+
function createGatewayRouter(config) {
|
|
732
|
+
let express;
|
|
733
|
+
try {
|
|
734
|
+
express = _require4("express");
|
|
735
|
+
} catch {
|
|
736
|
+
throw new Error("express is required for gateway router. Install it: npm install express");
|
|
737
|
+
}
|
|
738
|
+
const router = express.Router();
|
|
739
|
+
const remoteHealth = /* @__PURE__ */ new Map();
|
|
740
|
+
async function checkHealth(remote) {
|
|
741
|
+
try {
|
|
742
|
+
const path = remote.healthPath ?? "/agents";
|
|
743
|
+
const res = await fetch(`${remote.baseUrl}${path}`, {
|
|
744
|
+
headers: remote.headers ?? {},
|
|
745
|
+
signal: AbortSignal.timeout(5e3)
|
|
746
|
+
});
|
|
747
|
+
return res.ok;
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (config.healthCheckIntervalMs) {
|
|
753
|
+
const interval = setInterval(async () => {
|
|
754
|
+
for (const remote of config.remotes) {
|
|
755
|
+
const healthy = await checkHealth(remote);
|
|
756
|
+
remoteHealth.set(remote.baseUrl, healthy);
|
|
757
|
+
}
|
|
758
|
+
}, config.healthCheckIntervalMs);
|
|
759
|
+
interval.unref?.();
|
|
760
|
+
}
|
|
761
|
+
async function proxyRequest(baseUrl, path, req, res, headers) {
|
|
762
|
+
try {
|
|
763
|
+
const proxyRes = await fetch(`${baseUrl}${path}`, {
|
|
764
|
+
method: req.method,
|
|
765
|
+
headers: {
|
|
766
|
+
"Content-Type": "application/json",
|
|
767
|
+
...headers ?? {},
|
|
768
|
+
...Object.fromEntries(
|
|
769
|
+
Object.entries(req.headers).filter(([k]) => k.startsWith("x-") || k === "authorization")
|
|
770
|
+
)
|
|
771
|
+
},
|
|
772
|
+
body: req.method !== "GET" ? JSON.stringify(req.body) : void 0,
|
|
773
|
+
signal: AbortSignal.timeout(12e4)
|
|
774
|
+
});
|
|
775
|
+
const contentType = proxyRes.headers.get("content-type") ?? "";
|
|
776
|
+
if (contentType.includes("text/event-stream")) {
|
|
777
|
+
res.writeHead(200, {
|
|
778
|
+
"Content-Type": "text/event-stream",
|
|
779
|
+
"Cache-Control": "no-cache",
|
|
780
|
+
Connection: "keep-alive"
|
|
781
|
+
});
|
|
782
|
+
const reader = proxyRes.body?.getReader();
|
|
783
|
+
if (reader) {
|
|
784
|
+
const decoder = new TextDecoder();
|
|
785
|
+
while (true) {
|
|
786
|
+
const { done, value } = await reader.read();
|
|
787
|
+
if (done) break;
|
|
788
|
+
res.write(decoder.decode(value, { stream: true }));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
res.end();
|
|
792
|
+
} else {
|
|
793
|
+
const data = await proxyRes.json();
|
|
794
|
+
res.status(proxyRes.status).json(data);
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
res.status(502).json({ error: `Gateway proxy error: ${err.message}` });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const remote of config.remotes) {
|
|
801
|
+
if (remote.agents) {
|
|
802
|
+
for (const agentName of remote.agents) {
|
|
803
|
+
router.post(
|
|
804
|
+
`/agents/${agentName}/run`,
|
|
805
|
+
(req, res) => proxyRequest(remote.baseUrl, `/agents/${agentName}/run`, req, res, remote.headers)
|
|
806
|
+
);
|
|
807
|
+
router.post(
|
|
808
|
+
`/agents/${agentName}/stream`,
|
|
809
|
+
(req, res) => proxyRequest(remote.baseUrl, `/agents/${agentName}/stream`, req, res, remote.headers)
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (remote.teams) {
|
|
814
|
+
for (const teamName of remote.teams) {
|
|
815
|
+
router.post(
|
|
816
|
+
`/teams/${teamName}/run`,
|
|
817
|
+
(req, res) => proxyRequest(remote.baseUrl, `/teams/${teamName}/run`, req, res, remote.headers)
|
|
818
|
+
);
|
|
819
|
+
router.post(
|
|
820
|
+
`/teams/${teamName}/stream`,
|
|
821
|
+
(req, res) => proxyRequest(remote.baseUrl, `/teams/${teamName}/stream`, req, res, remote.headers)
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (remote.workflows) {
|
|
826
|
+
for (const wfName of remote.workflows) {
|
|
827
|
+
router.post(
|
|
828
|
+
`/workflows/${wfName}/run`,
|
|
829
|
+
(req, res) => proxyRequest(remote.baseUrl, `/workflows/${wfName}/run`, req, res, remote.headers)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
router.get("/gateway/health", async (_req, res) => {
|
|
835
|
+
const status = {};
|
|
836
|
+
for (const remote of config.remotes) {
|
|
837
|
+
const cached = remoteHealth.get(remote.baseUrl);
|
|
838
|
+
status[remote.baseUrl] = cached ?? await checkHealth(remote);
|
|
839
|
+
}
|
|
840
|
+
res.json({ remotes: status });
|
|
841
|
+
});
|
|
842
|
+
return router;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/express/jwt-middleware.ts
|
|
846
|
+
var import_node_module5 = require("module");
|
|
847
|
+
var _require5 = (0, import_node_module5.createRequire)(importMetaUrl);
|
|
848
|
+
function createJwtMiddleware(config) {
|
|
849
|
+
let jwt;
|
|
850
|
+
try {
|
|
851
|
+
jwt = _require5("jsonwebtoken");
|
|
852
|
+
} catch {
|
|
853
|
+
throw new Error("jsonwebtoken is required for JWT middleware. Install it: npm install jsonwebtoken");
|
|
854
|
+
}
|
|
855
|
+
const extractFrom = config.extractFrom ?? "header";
|
|
856
|
+
const cookieName = config.cookieName ?? "token";
|
|
857
|
+
return (req, res, next) => {
|
|
858
|
+
let token;
|
|
859
|
+
if (extractFrom === "header") {
|
|
860
|
+
const auth = req.headers.authorization;
|
|
861
|
+
if (auth?.startsWith("Bearer ")) {
|
|
862
|
+
token = auth.slice(7);
|
|
863
|
+
}
|
|
864
|
+
} else if (extractFrom === "cookie") {
|
|
865
|
+
token = req.cookies?.[cookieName];
|
|
866
|
+
}
|
|
867
|
+
if (!token) {
|
|
868
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const verifyOpts = {};
|
|
872
|
+
if (config.algorithm) verifyOpts.algorithms = [config.algorithm];
|
|
873
|
+
if (config.issuer) verifyOpts.issuer = config.issuer;
|
|
874
|
+
if (config.audience) verifyOpts.audience = config.audience;
|
|
875
|
+
const decoded = jwt.verify(token, config.secret, verifyOpts);
|
|
876
|
+
req.user = decoded;
|
|
877
|
+
next();
|
|
878
|
+
} catch (err) {
|
|
879
|
+
if (err.name === "TokenExpiredError") {
|
|
880
|
+
return res.status(401).json({ error: "Token expired" });
|
|
881
|
+
}
|
|
882
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
725
887
|
// src/express/middleware.ts
|
|
726
888
|
function errorHandler(options) {
|
|
727
889
|
const log = options?.logger ?? console;
|
|
@@ -743,16 +905,81 @@ function requestLogger(options) {
|
|
|
743
905
|
};
|
|
744
906
|
}
|
|
745
907
|
|
|
908
|
+
// src/express/rbac-middleware.ts
|
|
909
|
+
var DEFAULT_SCOPE_MAP = {
|
|
910
|
+
"POST /agents/:name/run": ["agents:run"],
|
|
911
|
+
"POST /agents/:name/stream": ["agents:run"],
|
|
912
|
+
"GET /agents": ["agents:read"],
|
|
913
|
+
"POST /teams/:name/run": ["teams:run"],
|
|
914
|
+
"POST /teams/:name/stream": ["teams:run"],
|
|
915
|
+
"GET /teams": ["teams:read"],
|
|
916
|
+
"POST /workflows/:name/run": ["workflows:run"],
|
|
917
|
+
"GET /workflows": ["workflows:read"],
|
|
918
|
+
"GET /admin": ["admin:*"],
|
|
919
|
+
"POST /admin": ["admin:*"],
|
|
920
|
+
"DELETE /admin": ["admin:*"]
|
|
921
|
+
};
|
|
922
|
+
function normalizeRoute(method, path) {
|
|
923
|
+
const normalized = path.replace(/\/[a-zA-Z0-9_-]+(?=\/(?:run|stream|card|checkpoints))/g, "/:name");
|
|
924
|
+
return `${method} ${normalized}`;
|
|
925
|
+
}
|
|
926
|
+
function createRbacMiddleware(config = {}) {
|
|
927
|
+
const scopeField = config.scopeField ?? "scopes";
|
|
928
|
+
const scopeMap = { ...DEFAULT_SCOPE_MAP, ...config.defaultScopes ?? {} };
|
|
929
|
+
return (req, res, next) => {
|
|
930
|
+
const user = req.user;
|
|
931
|
+
if (!user) {
|
|
932
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
933
|
+
}
|
|
934
|
+
const userScopes = user[scopeField] ?? user.scope?.split(" ") ?? [];
|
|
935
|
+
if (userScopes.includes("admin:*") || userScopes.includes("*")) {
|
|
936
|
+
return next();
|
|
937
|
+
}
|
|
938
|
+
const routeKey = normalizeRoute(req.method, req.path);
|
|
939
|
+
let requiredScopes = [];
|
|
940
|
+
for (const [pattern, scopes] of Object.entries(scopeMap)) {
|
|
941
|
+
if (routeKey === pattern || routeMatches(routeKey, pattern)) {
|
|
942
|
+
requiredScopes = scopes;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (config.agentScopes && req.params?.name) {
|
|
947
|
+
const agentSpecific = config.agentScopes[req.params.name];
|
|
948
|
+
if (agentSpecific) {
|
|
949
|
+
requiredScopes = [...requiredScopes, ...agentSpecific];
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (requiredScopes.length === 0) {
|
|
953
|
+
return next();
|
|
954
|
+
}
|
|
955
|
+
const hasRequired = requiredScopes.every((scope) => userScopes.includes(scope));
|
|
956
|
+
if (!hasRequired) {
|
|
957
|
+
return res.status(403).json({
|
|
958
|
+
error: "Insufficient permissions",
|
|
959
|
+
required: requiredScopes,
|
|
960
|
+
provided: userScopes
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
next();
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function routeMatches(actual, pattern) {
|
|
967
|
+
const actualParts = actual.split(/[\s/]+/).filter(Boolean);
|
|
968
|
+
const patternParts = pattern.split(/[\s/]+/).filter(Boolean);
|
|
969
|
+
if (actualParts.length !== patternParts.length) return false;
|
|
970
|
+
return patternParts.every((part, i) => part.startsWith(":") || part === actualParts[i]);
|
|
971
|
+
}
|
|
972
|
+
|
|
746
973
|
// src/express/router-factory.ts
|
|
747
|
-
var
|
|
974
|
+
var import_node_module7 = require("module");
|
|
748
975
|
var import_core3 = require("@radaros/core");
|
|
749
976
|
|
|
750
977
|
// src/express/swagger.ts
|
|
751
|
-
var
|
|
752
|
-
var
|
|
978
|
+
var import_node_module6 = require("module");
|
|
979
|
+
var _require6 = (0, import_node_module6.createRequire)(importMetaUrl);
|
|
753
980
|
function zodSchemaToJsonSchema(schema) {
|
|
754
981
|
try {
|
|
755
|
-
const zodToJsonSchema =
|
|
982
|
+
const zodToJsonSchema = _require6("zod-to-json-schema").default ?? _require6("zod-to-json-schema");
|
|
756
983
|
const result = zodToJsonSchema(schema, { target: "openApi3" });
|
|
757
984
|
const { $schema, ...rest } = result;
|
|
758
985
|
return rest;
|
|
@@ -1188,7 +1415,7 @@ ${agentDesc}`,
|
|
|
1188
1415
|
function serveSwaggerUI(spec) {
|
|
1189
1416
|
let swaggerUiExpress;
|
|
1190
1417
|
try {
|
|
1191
|
-
swaggerUiExpress =
|
|
1418
|
+
swaggerUiExpress = _require6("swagger-ui-express");
|
|
1192
1419
|
} catch {
|
|
1193
1420
|
throw new Error("swagger-ui-express is required for Swagger UI. Install it: npm install swagger-ui-express");
|
|
1194
1421
|
}
|
|
@@ -1202,7 +1429,7 @@ function serveSwaggerUI(spec) {
|
|
|
1202
1429
|
}
|
|
1203
1430
|
|
|
1204
1431
|
// src/express/router-factory.ts
|
|
1205
|
-
var
|
|
1432
|
+
var _require7 = (0, import_node_module7.createRequire)(importMetaUrl);
|
|
1206
1433
|
function corsMiddleware(origins) {
|
|
1207
1434
|
return (req, res, next) => {
|
|
1208
1435
|
const origin = req.headers.origin;
|
|
@@ -1302,7 +1529,7 @@ function createAgentRouter(opts) {
|
|
|
1302
1529
|
const reg = opts.registry === false ? null : opts.registry ?? import_core3.registry;
|
|
1303
1530
|
let express;
|
|
1304
1531
|
try {
|
|
1305
|
-
express =
|
|
1532
|
+
express = _require7("express");
|
|
1306
1533
|
} catch {
|
|
1307
1534
|
throw new Error("express is required for createAgentRouter. Install it: npm install express");
|
|
1308
1535
|
}
|
|
@@ -1314,6 +1541,12 @@ function createAgentRouter(opts) {
|
|
|
1314
1541
|
const config = opts.rateLimit === true ? {} : opts.rateLimit;
|
|
1315
1542
|
router.use(rateLimitMiddleware(config));
|
|
1316
1543
|
}
|
|
1544
|
+
if (opts.jwt) {
|
|
1545
|
+
router.use(createJwtMiddleware(opts.jwt));
|
|
1546
|
+
}
|
|
1547
|
+
if (opts.rbac) {
|
|
1548
|
+
router.use(createRbacMiddleware(opts.rbac));
|
|
1549
|
+
}
|
|
1317
1550
|
if (opts.middleware) {
|
|
1318
1551
|
for (const mw of opts.middleware) {
|
|
1319
1552
|
router.use(mw);
|
|
@@ -2344,6 +2577,9 @@ function createVoiceGateway(opts) {
|
|
|
2344
2577
|
createAgentRouter,
|
|
2345
2578
|
createBrowserGateway,
|
|
2346
2579
|
createFileUploadMiddleware,
|
|
2580
|
+
createGatewayRouter,
|
|
2581
|
+
createJwtMiddleware,
|
|
2582
|
+
createRbacMiddleware,
|
|
2347
2583
|
createVisionGateway,
|
|
2348
2584
|
createVoiceGateway,
|
|
2349
2585
|
errorHandler,
|
package/dist/index.d.cts
CHANGED
|
@@ -127,12 +127,22 @@ interface FileUploadOptions {
|
|
|
127
127
|
declare function createFileUploadMiddleware(opts?: FileUploadOptions): any;
|
|
128
128
|
declare function buildMultiModalInput(body: any, files?: any[]): string | any[];
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
interface RbacConfig {
|
|
131
|
+
scopeField?: string;
|
|
132
|
+
defaultScopes?: Record<string, string[]>;
|
|
133
|
+
agentScopes?: Record<string, string[]>;
|
|
134
|
+
}
|
|
135
|
+
declare function createRbacMiddleware(config?: RbacConfig): (req: any, res: any, next: any) => any;
|
|
136
|
+
|
|
137
|
+
interface JwtConfig {
|
|
138
|
+
secret: string;
|
|
139
|
+
algorithm?: string;
|
|
140
|
+
issuer?: string;
|
|
141
|
+
audience?: string;
|
|
142
|
+
extractFrom?: "header" | "cookie";
|
|
143
|
+
cookieName?: string;
|
|
144
|
+
}
|
|
145
|
+
declare function createJwtMiddleware(config: JwtConfig): (req: any, res: any, next: any) => any;
|
|
136
146
|
|
|
137
147
|
interface SwaggerOptions {
|
|
138
148
|
/** Enable Swagger UI at /docs. Default: false */
|
|
@@ -210,7 +220,39 @@ interface RouterOptions {
|
|
|
210
220
|
* MetricsExporter instance from `@radaros/observability` for `/metrics` endpoints.
|
|
211
221
|
*/
|
|
212
222
|
metricsExporter?: any;
|
|
223
|
+
/**
|
|
224
|
+
* JWT authentication middleware. Verifies tokens and attaches decoded payload to `req.user`.
|
|
225
|
+
* Requires `jsonwebtoken` package.
|
|
226
|
+
*/
|
|
227
|
+
jwt?: JwtConfig;
|
|
228
|
+
/**
|
|
229
|
+
* Role-based access control. Checks `req.user.scopes` against required scopes per route.
|
|
230
|
+
* Requires `jwt` to be configured first.
|
|
231
|
+
*/
|
|
232
|
+
rbac?: RbacConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface RemoteEndpoint {
|
|
236
|
+
baseUrl: string;
|
|
237
|
+
agents?: string[];
|
|
238
|
+
teams?: string[];
|
|
239
|
+
workflows?: string[];
|
|
240
|
+
headers?: Record<string, string>;
|
|
241
|
+
healthPath?: string;
|
|
213
242
|
}
|
|
243
|
+
interface GatewayConfig {
|
|
244
|
+
locals?: RouterOptions;
|
|
245
|
+
remotes: RemoteEndpoint[];
|
|
246
|
+
healthCheckIntervalMs?: number;
|
|
247
|
+
}
|
|
248
|
+
declare function createGatewayRouter(config: GatewayConfig): any;
|
|
249
|
+
|
|
250
|
+
declare function errorHandler(options?: {
|
|
251
|
+
logger?: Pick<Console, "error">;
|
|
252
|
+
}): (err: any, _req: any, res: any, _next: any) => void;
|
|
253
|
+
declare function requestLogger(options?: {
|
|
254
|
+
logger?: Pick<Console, "log">;
|
|
255
|
+
}): (req: any, _res: any, next: any) => void;
|
|
214
256
|
|
|
215
257
|
declare function createAgentRouter(opts: RouterOptions): any;
|
|
216
258
|
|
|
@@ -348,4 +390,4 @@ interface VoiceGatewayOptions {
|
|
|
348
390
|
}
|
|
349
391
|
declare function createVoiceGateway(opts: VoiceGatewayOptions): void;
|
|
350
392
|
|
|
351
|
-
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayOptions, MCPManager, type MCPServerEntry, type MCPServerSummary, type RouterOptions, type SwaggerOptions, type VisionGatewayOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, createVisionGateway, createVoiceGateway, errorHandler, generateAgentCard, generateMultiAgentCard, generateOpenAPISpec, requestLogger };
|
|
393
|
+
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayConfig, type GatewayOptions, type JwtConfig, MCPManager, type MCPServerEntry, type MCPServerSummary, type RbacConfig, type RemoteEndpoint, type RouterOptions, type SwaggerOptions, type VisionGatewayOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, createGatewayRouter, createJwtMiddleware, createRbacMiddleware, createVisionGateway, createVoiceGateway, errorHandler, generateAgentCard, generateMultiAgentCard, generateOpenAPISpec, requestLogger };
|
package/dist/index.d.ts
CHANGED
|
@@ -127,12 +127,22 @@ interface FileUploadOptions {
|
|
|
127
127
|
declare function createFileUploadMiddleware(opts?: FileUploadOptions): any;
|
|
128
128
|
declare function buildMultiModalInput(body: any, files?: any[]): string | any[];
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
interface RbacConfig {
|
|
131
|
+
scopeField?: string;
|
|
132
|
+
defaultScopes?: Record<string, string[]>;
|
|
133
|
+
agentScopes?: Record<string, string[]>;
|
|
134
|
+
}
|
|
135
|
+
declare function createRbacMiddleware(config?: RbacConfig): (req: any, res: any, next: any) => any;
|
|
136
|
+
|
|
137
|
+
interface JwtConfig {
|
|
138
|
+
secret: string;
|
|
139
|
+
algorithm?: string;
|
|
140
|
+
issuer?: string;
|
|
141
|
+
audience?: string;
|
|
142
|
+
extractFrom?: "header" | "cookie";
|
|
143
|
+
cookieName?: string;
|
|
144
|
+
}
|
|
145
|
+
declare function createJwtMiddleware(config: JwtConfig): (req: any, res: any, next: any) => any;
|
|
136
146
|
|
|
137
147
|
interface SwaggerOptions {
|
|
138
148
|
/** Enable Swagger UI at /docs. Default: false */
|
|
@@ -210,7 +220,39 @@ interface RouterOptions {
|
|
|
210
220
|
* MetricsExporter instance from `@radaros/observability` for `/metrics` endpoints.
|
|
211
221
|
*/
|
|
212
222
|
metricsExporter?: any;
|
|
223
|
+
/**
|
|
224
|
+
* JWT authentication middleware. Verifies tokens and attaches decoded payload to `req.user`.
|
|
225
|
+
* Requires `jsonwebtoken` package.
|
|
226
|
+
*/
|
|
227
|
+
jwt?: JwtConfig;
|
|
228
|
+
/**
|
|
229
|
+
* Role-based access control. Checks `req.user.scopes` against required scopes per route.
|
|
230
|
+
* Requires `jwt` to be configured first.
|
|
231
|
+
*/
|
|
232
|
+
rbac?: RbacConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface RemoteEndpoint {
|
|
236
|
+
baseUrl: string;
|
|
237
|
+
agents?: string[];
|
|
238
|
+
teams?: string[];
|
|
239
|
+
workflows?: string[];
|
|
240
|
+
headers?: Record<string, string>;
|
|
241
|
+
healthPath?: string;
|
|
213
242
|
}
|
|
243
|
+
interface GatewayConfig {
|
|
244
|
+
locals?: RouterOptions;
|
|
245
|
+
remotes: RemoteEndpoint[];
|
|
246
|
+
healthCheckIntervalMs?: number;
|
|
247
|
+
}
|
|
248
|
+
declare function createGatewayRouter(config: GatewayConfig): any;
|
|
249
|
+
|
|
250
|
+
declare function errorHandler(options?: {
|
|
251
|
+
logger?: Pick<Console, "error">;
|
|
252
|
+
}): (err: any, _req: any, res: any, _next: any) => void;
|
|
253
|
+
declare function requestLogger(options?: {
|
|
254
|
+
logger?: Pick<Console, "log">;
|
|
255
|
+
}): (req: any, _res: any, next: any) => void;
|
|
214
256
|
|
|
215
257
|
declare function createAgentRouter(opts: RouterOptions): any;
|
|
216
258
|
|
|
@@ -348,4 +390,4 @@ interface VoiceGatewayOptions {
|
|
|
348
390
|
}
|
|
349
391
|
declare function createVoiceGateway(opts: VoiceGatewayOptions): void;
|
|
350
392
|
|
|
351
|
-
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayOptions, MCPManager, type MCPServerEntry, type MCPServerSummary, type RouterOptions, type SwaggerOptions, type VisionGatewayOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, createVisionGateway, createVoiceGateway, errorHandler, generateAgentCard, generateMultiAgentCard, generateOpenAPISpec, requestLogger };
|
|
393
|
+
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayConfig, type GatewayOptions, type JwtConfig, MCPManager, type MCPServerEntry, type MCPServerSummary, type RbacConfig, type RemoteEndpoint, type RouterOptions, type SwaggerOptions, type VisionGatewayOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, createGatewayRouter, createJwtMiddleware, createRbacMiddleware, createVisionGateway, createVoiceGateway, errorHandler, generateAgentCard, generateMultiAgentCard, generateOpenAPISpec, requestLogger };
|
package/dist/index.js
CHANGED
|
@@ -678,6 +678,165 @@ function buildMultiModalInput(body, files) {
|
|
|
678
678
|
return parts;
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
+
// src/express/gateway.ts
|
|
682
|
+
import { createRequire as createRequire4 } from "module";
|
|
683
|
+
var _require4 = createRequire4(import.meta.url);
|
|
684
|
+
function createGatewayRouter(config) {
|
|
685
|
+
let express;
|
|
686
|
+
try {
|
|
687
|
+
express = _require4("express");
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error("express is required for gateway router. Install it: npm install express");
|
|
690
|
+
}
|
|
691
|
+
const router = express.Router();
|
|
692
|
+
const remoteHealth = /* @__PURE__ */ new Map();
|
|
693
|
+
async function checkHealth(remote) {
|
|
694
|
+
try {
|
|
695
|
+
const path = remote.healthPath ?? "/agents";
|
|
696
|
+
const res = await fetch(`${remote.baseUrl}${path}`, {
|
|
697
|
+
headers: remote.headers ?? {},
|
|
698
|
+
signal: AbortSignal.timeout(5e3)
|
|
699
|
+
});
|
|
700
|
+
return res.ok;
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (config.healthCheckIntervalMs) {
|
|
706
|
+
const interval = setInterval(async () => {
|
|
707
|
+
for (const remote of config.remotes) {
|
|
708
|
+
const healthy = await checkHealth(remote);
|
|
709
|
+
remoteHealth.set(remote.baseUrl, healthy);
|
|
710
|
+
}
|
|
711
|
+
}, config.healthCheckIntervalMs);
|
|
712
|
+
interval.unref?.();
|
|
713
|
+
}
|
|
714
|
+
async function proxyRequest(baseUrl, path, req, res, headers) {
|
|
715
|
+
try {
|
|
716
|
+
const proxyRes = await fetch(`${baseUrl}${path}`, {
|
|
717
|
+
method: req.method,
|
|
718
|
+
headers: {
|
|
719
|
+
"Content-Type": "application/json",
|
|
720
|
+
...headers ?? {},
|
|
721
|
+
...Object.fromEntries(
|
|
722
|
+
Object.entries(req.headers).filter(([k]) => k.startsWith("x-") || k === "authorization")
|
|
723
|
+
)
|
|
724
|
+
},
|
|
725
|
+
body: req.method !== "GET" ? JSON.stringify(req.body) : void 0,
|
|
726
|
+
signal: AbortSignal.timeout(12e4)
|
|
727
|
+
});
|
|
728
|
+
const contentType = proxyRes.headers.get("content-type") ?? "";
|
|
729
|
+
if (contentType.includes("text/event-stream")) {
|
|
730
|
+
res.writeHead(200, {
|
|
731
|
+
"Content-Type": "text/event-stream",
|
|
732
|
+
"Cache-Control": "no-cache",
|
|
733
|
+
Connection: "keep-alive"
|
|
734
|
+
});
|
|
735
|
+
const reader = proxyRes.body?.getReader();
|
|
736
|
+
if (reader) {
|
|
737
|
+
const decoder = new TextDecoder();
|
|
738
|
+
while (true) {
|
|
739
|
+
const { done, value } = await reader.read();
|
|
740
|
+
if (done) break;
|
|
741
|
+
res.write(decoder.decode(value, { stream: true }));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
res.end();
|
|
745
|
+
} else {
|
|
746
|
+
const data = await proxyRes.json();
|
|
747
|
+
res.status(proxyRes.status).json(data);
|
|
748
|
+
}
|
|
749
|
+
} catch (err) {
|
|
750
|
+
res.status(502).json({ error: `Gateway proxy error: ${err.message}` });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
for (const remote of config.remotes) {
|
|
754
|
+
if (remote.agents) {
|
|
755
|
+
for (const agentName of remote.agents) {
|
|
756
|
+
router.post(
|
|
757
|
+
`/agents/${agentName}/run`,
|
|
758
|
+
(req, res) => proxyRequest(remote.baseUrl, `/agents/${agentName}/run`, req, res, remote.headers)
|
|
759
|
+
);
|
|
760
|
+
router.post(
|
|
761
|
+
`/agents/${agentName}/stream`,
|
|
762
|
+
(req, res) => proxyRequest(remote.baseUrl, `/agents/${agentName}/stream`, req, res, remote.headers)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (remote.teams) {
|
|
767
|
+
for (const teamName of remote.teams) {
|
|
768
|
+
router.post(
|
|
769
|
+
`/teams/${teamName}/run`,
|
|
770
|
+
(req, res) => proxyRequest(remote.baseUrl, `/teams/${teamName}/run`, req, res, remote.headers)
|
|
771
|
+
);
|
|
772
|
+
router.post(
|
|
773
|
+
`/teams/${teamName}/stream`,
|
|
774
|
+
(req, res) => proxyRequest(remote.baseUrl, `/teams/${teamName}/stream`, req, res, remote.headers)
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (remote.workflows) {
|
|
779
|
+
for (const wfName of remote.workflows) {
|
|
780
|
+
router.post(
|
|
781
|
+
`/workflows/${wfName}/run`,
|
|
782
|
+
(req, res) => proxyRequest(remote.baseUrl, `/workflows/${wfName}/run`, req, res, remote.headers)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
router.get("/gateway/health", async (_req, res) => {
|
|
788
|
+
const status = {};
|
|
789
|
+
for (const remote of config.remotes) {
|
|
790
|
+
const cached = remoteHealth.get(remote.baseUrl);
|
|
791
|
+
status[remote.baseUrl] = cached ?? await checkHealth(remote);
|
|
792
|
+
}
|
|
793
|
+
res.json({ remotes: status });
|
|
794
|
+
});
|
|
795
|
+
return router;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/express/jwt-middleware.ts
|
|
799
|
+
import { createRequire as createRequire5 } from "module";
|
|
800
|
+
var _require5 = createRequire5(import.meta.url);
|
|
801
|
+
function createJwtMiddleware(config) {
|
|
802
|
+
let jwt;
|
|
803
|
+
try {
|
|
804
|
+
jwt = _require5("jsonwebtoken");
|
|
805
|
+
} catch {
|
|
806
|
+
throw new Error("jsonwebtoken is required for JWT middleware. Install it: npm install jsonwebtoken");
|
|
807
|
+
}
|
|
808
|
+
const extractFrom = config.extractFrom ?? "header";
|
|
809
|
+
const cookieName = config.cookieName ?? "token";
|
|
810
|
+
return (req, res, next) => {
|
|
811
|
+
let token;
|
|
812
|
+
if (extractFrom === "header") {
|
|
813
|
+
const auth = req.headers.authorization;
|
|
814
|
+
if (auth?.startsWith("Bearer ")) {
|
|
815
|
+
token = auth.slice(7);
|
|
816
|
+
}
|
|
817
|
+
} else if (extractFrom === "cookie") {
|
|
818
|
+
token = req.cookies?.[cookieName];
|
|
819
|
+
}
|
|
820
|
+
if (!token) {
|
|
821
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const verifyOpts = {};
|
|
825
|
+
if (config.algorithm) verifyOpts.algorithms = [config.algorithm];
|
|
826
|
+
if (config.issuer) verifyOpts.issuer = config.issuer;
|
|
827
|
+
if (config.audience) verifyOpts.audience = config.audience;
|
|
828
|
+
const decoded = jwt.verify(token, config.secret, verifyOpts);
|
|
829
|
+
req.user = decoded;
|
|
830
|
+
next();
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (err.name === "TokenExpiredError") {
|
|
833
|
+
return res.status(401).json({ error: "Token expired" });
|
|
834
|
+
}
|
|
835
|
+
return res.status(401).json({ error: "Invalid token" });
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
681
840
|
// src/express/middleware.ts
|
|
682
841
|
function errorHandler(options) {
|
|
683
842
|
const log = options?.logger ?? console;
|
|
@@ -699,16 +858,81 @@ function requestLogger(options) {
|
|
|
699
858
|
};
|
|
700
859
|
}
|
|
701
860
|
|
|
861
|
+
// src/express/rbac-middleware.ts
|
|
862
|
+
var DEFAULT_SCOPE_MAP = {
|
|
863
|
+
"POST /agents/:name/run": ["agents:run"],
|
|
864
|
+
"POST /agents/:name/stream": ["agents:run"],
|
|
865
|
+
"GET /agents": ["agents:read"],
|
|
866
|
+
"POST /teams/:name/run": ["teams:run"],
|
|
867
|
+
"POST /teams/:name/stream": ["teams:run"],
|
|
868
|
+
"GET /teams": ["teams:read"],
|
|
869
|
+
"POST /workflows/:name/run": ["workflows:run"],
|
|
870
|
+
"GET /workflows": ["workflows:read"],
|
|
871
|
+
"GET /admin": ["admin:*"],
|
|
872
|
+
"POST /admin": ["admin:*"],
|
|
873
|
+
"DELETE /admin": ["admin:*"]
|
|
874
|
+
};
|
|
875
|
+
function normalizeRoute(method, path) {
|
|
876
|
+
const normalized = path.replace(/\/[a-zA-Z0-9_-]+(?=\/(?:run|stream|card|checkpoints))/g, "/:name");
|
|
877
|
+
return `${method} ${normalized}`;
|
|
878
|
+
}
|
|
879
|
+
function createRbacMiddleware(config = {}) {
|
|
880
|
+
const scopeField = config.scopeField ?? "scopes";
|
|
881
|
+
const scopeMap = { ...DEFAULT_SCOPE_MAP, ...config.defaultScopes ?? {} };
|
|
882
|
+
return (req, res, next) => {
|
|
883
|
+
const user = req.user;
|
|
884
|
+
if (!user) {
|
|
885
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
886
|
+
}
|
|
887
|
+
const userScopes = user[scopeField] ?? user.scope?.split(" ") ?? [];
|
|
888
|
+
if (userScopes.includes("admin:*") || userScopes.includes("*")) {
|
|
889
|
+
return next();
|
|
890
|
+
}
|
|
891
|
+
const routeKey = normalizeRoute(req.method, req.path);
|
|
892
|
+
let requiredScopes = [];
|
|
893
|
+
for (const [pattern, scopes] of Object.entries(scopeMap)) {
|
|
894
|
+
if (routeKey === pattern || routeMatches(routeKey, pattern)) {
|
|
895
|
+
requiredScopes = scopes;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (config.agentScopes && req.params?.name) {
|
|
900
|
+
const agentSpecific = config.agentScopes[req.params.name];
|
|
901
|
+
if (agentSpecific) {
|
|
902
|
+
requiredScopes = [...requiredScopes, ...agentSpecific];
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (requiredScopes.length === 0) {
|
|
906
|
+
return next();
|
|
907
|
+
}
|
|
908
|
+
const hasRequired = requiredScopes.every((scope) => userScopes.includes(scope));
|
|
909
|
+
if (!hasRequired) {
|
|
910
|
+
return res.status(403).json({
|
|
911
|
+
error: "Insufficient permissions",
|
|
912
|
+
required: requiredScopes,
|
|
913
|
+
provided: userScopes
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
next();
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function routeMatches(actual, pattern) {
|
|
920
|
+
const actualParts = actual.split(/[\s/]+/).filter(Boolean);
|
|
921
|
+
const patternParts = pattern.split(/[\s/]+/).filter(Boolean);
|
|
922
|
+
if (actualParts.length !== patternParts.length) return false;
|
|
923
|
+
return patternParts.every((part, i) => part.startsWith(":") || part === actualParts[i]);
|
|
924
|
+
}
|
|
925
|
+
|
|
702
926
|
// src/express/router-factory.ts
|
|
703
|
-
import { createRequire as
|
|
927
|
+
import { createRequire as createRequire7 } from "module";
|
|
704
928
|
import { classifyServables, collectToolkitTools, describeToolLibrary, registry as globalRegistry } from "@radaros/core";
|
|
705
929
|
|
|
706
930
|
// src/express/swagger.ts
|
|
707
|
-
import { createRequire as
|
|
708
|
-
var
|
|
931
|
+
import { createRequire as createRequire6 } from "module";
|
|
932
|
+
var _require6 = createRequire6(import.meta.url);
|
|
709
933
|
function zodSchemaToJsonSchema(schema) {
|
|
710
934
|
try {
|
|
711
|
-
const zodToJsonSchema =
|
|
935
|
+
const zodToJsonSchema = _require6("zod-to-json-schema").default ?? _require6("zod-to-json-schema");
|
|
712
936
|
const result = zodToJsonSchema(schema, { target: "openApi3" });
|
|
713
937
|
const { $schema, ...rest } = result;
|
|
714
938
|
return rest;
|
|
@@ -1144,7 +1368,7 @@ ${agentDesc}`,
|
|
|
1144
1368
|
function serveSwaggerUI(spec) {
|
|
1145
1369
|
let swaggerUiExpress;
|
|
1146
1370
|
try {
|
|
1147
|
-
swaggerUiExpress =
|
|
1371
|
+
swaggerUiExpress = _require6("swagger-ui-express");
|
|
1148
1372
|
} catch {
|
|
1149
1373
|
throw new Error("swagger-ui-express is required for Swagger UI. Install it: npm install swagger-ui-express");
|
|
1150
1374
|
}
|
|
@@ -1158,7 +1382,7 @@ function serveSwaggerUI(spec) {
|
|
|
1158
1382
|
}
|
|
1159
1383
|
|
|
1160
1384
|
// src/express/router-factory.ts
|
|
1161
|
-
var
|
|
1385
|
+
var _require7 = createRequire7(import.meta.url);
|
|
1162
1386
|
function corsMiddleware(origins) {
|
|
1163
1387
|
return (req, res, next) => {
|
|
1164
1388
|
const origin = req.headers.origin;
|
|
@@ -1258,7 +1482,7 @@ function createAgentRouter(opts) {
|
|
|
1258
1482
|
const reg = opts.registry === false ? null : opts.registry ?? globalRegistry;
|
|
1259
1483
|
let express;
|
|
1260
1484
|
try {
|
|
1261
|
-
express =
|
|
1485
|
+
express = _require7("express");
|
|
1262
1486
|
} catch {
|
|
1263
1487
|
throw new Error("express is required for createAgentRouter. Install it: npm install express");
|
|
1264
1488
|
}
|
|
@@ -1270,6 +1494,12 @@ function createAgentRouter(opts) {
|
|
|
1270
1494
|
const config = opts.rateLimit === true ? {} : opts.rateLimit;
|
|
1271
1495
|
router.use(rateLimitMiddleware(config));
|
|
1272
1496
|
}
|
|
1497
|
+
if (opts.jwt) {
|
|
1498
|
+
router.use(createJwtMiddleware(opts.jwt));
|
|
1499
|
+
}
|
|
1500
|
+
if (opts.rbac) {
|
|
1501
|
+
router.use(createRbacMiddleware(opts.rbac));
|
|
1502
|
+
}
|
|
1273
1503
|
if (opts.middleware) {
|
|
1274
1504
|
for (const mw of opts.middleware) {
|
|
1275
1505
|
router.use(mw);
|
|
@@ -2299,6 +2529,9 @@ export {
|
|
|
2299
2529
|
createAgentRouter,
|
|
2300
2530
|
createBrowserGateway,
|
|
2301
2531
|
createFileUploadMiddleware,
|
|
2532
|
+
createGatewayRouter,
|
|
2533
|
+
createJwtMiddleware,
|
|
2534
|
+
createRbacMiddleware,
|
|
2302
2535
|
createVisionGateway,
|
|
2303
2536
|
createVoiceGateway,
|
|
2304
2537
|
errorHandler,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radaros/transport",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.49",
|
|
4
4
|
"description": "HTTP and WebSocket transport layer for RadarOS agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typescript": "^5.6.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@radaros/core": "^0.3.
|
|
45
|
+
"@radaros/core": "^0.3.49",
|
|
46
46
|
"@types/express": "^4.0.0 || ^5.0.0",
|
|
47
47
|
"express": "^4.0.0 || ^5.0.0",
|
|
48
48
|
"multer": ">=2.0.0",
|