@radaros/transport 0.3.45 → 0.3.48
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 +367 -7
- package/dist/index.d.cts +58 -8
- package/dist/index.d.ts +58 -8
- package/dist/index.js +363 -7
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,10 @@ __export(index_exports, {
|
|
|
28
28
|
createAgentRouter: () => createAgentRouter,
|
|
29
29
|
createBrowserGateway: () => createBrowserGateway,
|
|
30
30
|
createFileUploadMiddleware: () => createFileUploadMiddleware,
|
|
31
|
+
createGatewayRouter: () => createGatewayRouter,
|
|
32
|
+
createJwtMiddleware: () => createJwtMiddleware,
|
|
33
|
+
createRbacMiddleware: () => createRbacMiddleware,
|
|
34
|
+
createVisionGateway: () => createVisionGateway,
|
|
31
35
|
createVoiceGateway: () => createVoiceGateway,
|
|
32
36
|
errorHandler: () => errorHandler,
|
|
33
37
|
generateAgentCard: () => generateAgentCard,
|
|
@@ -721,6 +725,165 @@ function buildMultiModalInput(body, files) {
|
|
|
721
725
|
return parts;
|
|
722
726
|
}
|
|
723
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
|
+
|
|
724
887
|
// src/express/middleware.ts
|
|
725
888
|
function errorHandler(options) {
|
|
726
889
|
const log = options?.logger ?? console;
|
|
@@ -742,16 +905,81 @@ function requestLogger(options) {
|
|
|
742
905
|
};
|
|
743
906
|
}
|
|
744
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
|
+
|
|
745
973
|
// src/express/router-factory.ts
|
|
746
|
-
var
|
|
974
|
+
var import_node_module7 = require("module");
|
|
747
975
|
var import_core3 = require("@radaros/core");
|
|
748
976
|
|
|
749
977
|
// src/express/swagger.ts
|
|
750
|
-
var
|
|
751
|
-
var
|
|
978
|
+
var import_node_module6 = require("module");
|
|
979
|
+
var _require6 = (0, import_node_module6.createRequire)(importMetaUrl);
|
|
752
980
|
function zodSchemaToJsonSchema(schema) {
|
|
753
981
|
try {
|
|
754
|
-
const zodToJsonSchema =
|
|
982
|
+
const zodToJsonSchema = _require6("zod-to-json-schema").default ?? _require6("zod-to-json-schema");
|
|
755
983
|
const result = zodToJsonSchema(schema, { target: "openApi3" });
|
|
756
984
|
const { $schema, ...rest } = result;
|
|
757
985
|
return rest;
|
|
@@ -1187,7 +1415,7 @@ ${agentDesc}`,
|
|
|
1187
1415
|
function serveSwaggerUI(spec) {
|
|
1188
1416
|
let swaggerUiExpress;
|
|
1189
1417
|
try {
|
|
1190
|
-
swaggerUiExpress =
|
|
1418
|
+
swaggerUiExpress = _require6("swagger-ui-express");
|
|
1191
1419
|
} catch {
|
|
1192
1420
|
throw new Error("swagger-ui-express is required for Swagger UI. Install it: npm install swagger-ui-express");
|
|
1193
1421
|
}
|
|
@@ -1201,7 +1429,7 @@ function serveSwaggerUI(spec) {
|
|
|
1201
1429
|
}
|
|
1202
1430
|
|
|
1203
1431
|
// src/express/router-factory.ts
|
|
1204
|
-
var
|
|
1432
|
+
var _require7 = (0, import_node_module7.createRequire)(importMetaUrl);
|
|
1205
1433
|
function corsMiddleware(origins) {
|
|
1206
1434
|
return (req, res, next) => {
|
|
1207
1435
|
const origin = req.headers.origin;
|
|
@@ -1301,7 +1529,7 @@ function createAgentRouter(opts) {
|
|
|
1301
1529
|
const reg = opts.registry === false ? null : opts.registry ?? import_core3.registry;
|
|
1302
1530
|
let express;
|
|
1303
1531
|
try {
|
|
1304
|
-
express =
|
|
1532
|
+
express = _require7("express");
|
|
1305
1533
|
} catch {
|
|
1306
1534
|
throw new Error("express is required for createAgentRouter. Install it: npm install express");
|
|
1307
1535
|
}
|
|
@@ -1313,6 +1541,12 @@ function createAgentRouter(opts) {
|
|
|
1313
1541
|
const config = opts.rateLimit === true ? {} : opts.rateLimit;
|
|
1314
1542
|
router.use(rateLimitMiddleware(config));
|
|
1315
1543
|
}
|
|
1544
|
+
if (opts.jwt) {
|
|
1545
|
+
router.use(createJwtMiddleware(opts.jwt));
|
|
1546
|
+
}
|
|
1547
|
+
if (opts.rbac) {
|
|
1548
|
+
router.use(createRbacMiddleware(opts.rbac));
|
|
1549
|
+
}
|
|
1316
1550
|
if (opts.middleware) {
|
|
1317
1551
|
for (const mw of opts.middleware) {
|
|
1318
1552
|
router.use(mw);
|
|
@@ -2080,6 +2314,128 @@ function createAgentGateway(opts) {
|
|
|
2080
2314
|
});
|
|
2081
2315
|
}
|
|
2082
2316
|
|
|
2317
|
+
// src/socketio/vision-gateway.ts
|
|
2318
|
+
function createVisionGateway(opts) {
|
|
2319
|
+
const ns = opts.io.of(opts.namespace ?? "/radaros-vision");
|
|
2320
|
+
if (opts.authMiddleware) {
|
|
2321
|
+
ns.use(opts.authMiddleware);
|
|
2322
|
+
}
|
|
2323
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
2324
|
+
ns.on("connection", (socket) => {
|
|
2325
|
+
socket.on(
|
|
2326
|
+
"vision.start",
|
|
2327
|
+
async (data) => {
|
|
2328
|
+
const agent = opts.agents[data.agentName];
|
|
2329
|
+
if (!agent) {
|
|
2330
|
+
socket.emit("vision.error", { error: `Vision agent "${data.agentName}" not found` });
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
if (activeSessions.has(socket.id)) {
|
|
2334
|
+
socket.emit("vision.error", { error: "A vision session is already active for this connection" });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
|
|
2339
|
+
const userId = data.userId ?? socket.handshake?.auth?.userId;
|
|
2340
|
+
const sessionId = data.sessionId ?? socket.handshake?.auth?.sessionId;
|
|
2341
|
+
const session = await agent.connect({ apiKey, userId, sessionId });
|
|
2342
|
+
activeSessions.set(socket.id, session);
|
|
2343
|
+
session.on("audio", (ev) => {
|
|
2344
|
+
socket.emit("vision.audio", {
|
|
2345
|
+
data: ev.data.toString("base64"),
|
|
2346
|
+
mimeType: ev.mimeType ?? "audio/pcm"
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
session.on("transcript", (ev) => {
|
|
2350
|
+
socket.emit("vision.transcript", { text: ev.text, role: ev.role });
|
|
2351
|
+
});
|
|
2352
|
+
session.on("text", (ev) => {
|
|
2353
|
+
socket.emit("vision.text", { text: ev.text });
|
|
2354
|
+
});
|
|
2355
|
+
session.on("tool_call_start", (ev) => {
|
|
2356
|
+
socket.emit("vision.tool.call", { name: ev.name, args: ev.args });
|
|
2357
|
+
});
|
|
2358
|
+
session.on("tool_result", (ev) => {
|
|
2359
|
+
socket.emit("vision.tool.result", { name: ev.name, result: ev.result });
|
|
2360
|
+
});
|
|
2361
|
+
session.on("usage", (ev) => {
|
|
2362
|
+
socket.emit("vision.usage", ev);
|
|
2363
|
+
});
|
|
2364
|
+
session.on("interrupted", () => {
|
|
2365
|
+
socket.emit("vision.interrupted");
|
|
2366
|
+
});
|
|
2367
|
+
session.on("error", (ev) => {
|
|
2368
|
+
socket.emit("vision.error", { error: ev.error.message });
|
|
2369
|
+
});
|
|
2370
|
+
session.on("disconnected", () => {
|
|
2371
|
+
activeSessions.delete(socket.id);
|
|
2372
|
+
socket.emit("vision.stopped");
|
|
2373
|
+
});
|
|
2374
|
+
socket.emit("vision.started", { userId });
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
socket.emit("vision.error", { error: error.message });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
);
|
|
2380
|
+
socket.on("vision.audio", (data) => {
|
|
2381
|
+
const session = activeSessions.get(socket.id);
|
|
2382
|
+
if (!session) return;
|
|
2383
|
+
if (typeof data?.data !== "string" || data.data.length > 1e6) return;
|
|
2384
|
+
try {
|
|
2385
|
+
session.sendAudio(Buffer.from(data.data, "base64"));
|
|
2386
|
+
} catch {
|
|
2387
|
+
socket.emit("vision.error", { error: "Invalid audio data" });
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
socket.on("vision.image", (data) => {
|
|
2391
|
+
const session = activeSessions.get(socket.id);
|
|
2392
|
+
if (!session) return;
|
|
2393
|
+
if (typeof data?.data !== "string" || data.data.length > 5e6) return;
|
|
2394
|
+
try {
|
|
2395
|
+
session.sendImage(Buffer.from(data.data, "base64"), data.mimeType ?? "image/jpeg");
|
|
2396
|
+
} catch {
|
|
2397
|
+
socket.emit("vision.error", { error: "Invalid image data" });
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
socket.on("vision.text", (data) => {
|
|
2401
|
+
const session = activeSessions.get(socket.id);
|
|
2402
|
+
if (!session) return;
|
|
2403
|
+
if (typeof data?.text !== "string" || data.text.length > 1e4) return;
|
|
2404
|
+
session.sendText(data.text);
|
|
2405
|
+
});
|
|
2406
|
+
socket.on("vision.interrupt", () => {
|
|
2407
|
+
const session = activeSessions.get(socket.id);
|
|
2408
|
+
if (!session) return;
|
|
2409
|
+
session.interrupt();
|
|
2410
|
+
});
|
|
2411
|
+
socket.on("vision.stop", async () => {
|
|
2412
|
+
const session = activeSessions.get(socket.id);
|
|
2413
|
+
if (!session) return;
|
|
2414
|
+
try {
|
|
2415
|
+
await session.close();
|
|
2416
|
+
} catch (err) {
|
|
2417
|
+
console.warn("[vision-gateway] Error closing session:", err);
|
|
2418
|
+
}
|
|
2419
|
+
activeSessions.delete(socket.id);
|
|
2420
|
+
socket.emit("vision.stopped");
|
|
2421
|
+
});
|
|
2422
|
+
socket.on("disconnect", async () => {
|
|
2423
|
+
const session = activeSessions.get(socket.id);
|
|
2424
|
+
if (session) {
|
|
2425
|
+
try {
|
|
2426
|
+
await session.close();
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
console.warn(
|
|
2429
|
+
"[radaros/vision-gateway] Error closing session on disconnect:",
|
|
2430
|
+
err instanceof Error ? err.message : err
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
activeSessions.delete(socket.id);
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2083
2439
|
// src/socketio/voice-gateway.ts
|
|
2084
2440
|
function createVoiceGateway(opts) {
|
|
2085
2441
|
const ns = opts.io.of(opts.namespace ?? "/radaros-voice");
|
|
@@ -2221,6 +2577,10 @@ function createVoiceGateway(opts) {
|
|
|
2221
2577
|
createAgentRouter,
|
|
2222
2578
|
createBrowserGateway,
|
|
2223
2579
|
createFileUploadMiddleware,
|
|
2580
|
+
createGatewayRouter,
|
|
2581
|
+
createJwtMiddleware,
|
|
2582
|
+
createRbacMiddleware,
|
|
2583
|
+
createVisionGateway,
|
|
2224
2584
|
createVoiceGateway,
|
|
2225
2585
|
errorHandler,
|
|
2226
2586
|
generateAgentCard,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Agent, A2AAgentCard, MCPToolProviderConfig, ToolDef, MCPToolProvider, Registry, Servable, Team, Workflow, Toolkit, EventBus, VoiceAgent } from '@radaros/core';
|
|
1
|
+
import { Agent, A2AAgentCard, MCPToolProviderConfig, ToolDef, MCPToolProvider, Registry, Servable, Team, Workflow, Toolkit, EventBus, VisionAgent, VoiceAgent } from '@radaros/core';
|
|
2
2
|
|
|
3
3
|
interface A2AServerOptions {
|
|
4
4
|
agents: Record<string, Agent>;
|
|
@@ -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;
|
|
242
|
+
}
|
|
243
|
+
interface GatewayConfig {
|
|
244
|
+
locals?: RouterOptions;
|
|
245
|
+
remotes: RemoteEndpoint[];
|
|
246
|
+
healthCheckIntervalMs?: number;
|
|
213
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
|
|
|
@@ -332,6 +374,14 @@ interface GatewayOptions {
|
|
|
332
374
|
|
|
333
375
|
declare function createAgentGateway(opts: GatewayOptions): void;
|
|
334
376
|
|
|
377
|
+
interface VisionGatewayOptions {
|
|
378
|
+
agents: Record<string, VisionAgent>;
|
|
379
|
+
io: any;
|
|
380
|
+
namespace?: string;
|
|
381
|
+
authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
|
|
382
|
+
}
|
|
383
|
+
declare function createVisionGateway(opts: VisionGatewayOptions): void;
|
|
384
|
+
|
|
335
385
|
interface VoiceGatewayOptions {
|
|
336
386
|
agents: Record<string, VoiceAgent>;
|
|
337
387
|
io: any;
|
|
@@ -340,4 +390,4 @@ interface VoiceGatewayOptions {
|
|
|
340
390
|
}
|
|
341
391
|
declare function createVoiceGateway(opts: VoiceGatewayOptions): void;
|
|
342
392
|
|
|
343
|
-
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayOptions, MCPManager, type MCPServerEntry, type MCPServerSummary, type RouterOptions, type SwaggerOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Agent, A2AAgentCard, MCPToolProviderConfig, ToolDef, MCPToolProvider, Registry, Servable, Team, Workflow, Toolkit, EventBus, VoiceAgent } from '@radaros/core';
|
|
1
|
+
import { Agent, A2AAgentCard, MCPToolProviderConfig, ToolDef, MCPToolProvider, Registry, Servable, Team, Workflow, Toolkit, EventBus, VisionAgent, VoiceAgent } from '@radaros/core';
|
|
2
2
|
|
|
3
3
|
interface A2AServerOptions {
|
|
4
4
|
agents: Record<string, Agent>;
|
|
@@ -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;
|
|
242
|
+
}
|
|
243
|
+
interface GatewayConfig {
|
|
244
|
+
locals?: RouterOptions;
|
|
245
|
+
remotes: RemoteEndpoint[];
|
|
246
|
+
healthCheckIntervalMs?: number;
|
|
213
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
|
|
|
@@ -332,6 +374,14 @@ interface GatewayOptions {
|
|
|
332
374
|
|
|
333
375
|
declare function createAgentGateway(opts: GatewayOptions): void;
|
|
334
376
|
|
|
377
|
+
interface VisionGatewayOptions {
|
|
378
|
+
agents: Record<string, VisionAgent>;
|
|
379
|
+
io: any;
|
|
380
|
+
namespace?: string;
|
|
381
|
+
authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
|
|
382
|
+
}
|
|
383
|
+
declare function createVisionGateway(opts: VisionGatewayOptions): void;
|
|
384
|
+
|
|
335
385
|
interface VoiceGatewayOptions {
|
|
336
386
|
agents: Record<string, VoiceAgent>;
|
|
337
387
|
io: any;
|
|
@@ -340,4 +390,4 @@ interface VoiceGatewayOptions {
|
|
|
340
390
|
}
|
|
341
391
|
declare function createVoiceGateway(opts: VoiceGatewayOptions): void;
|
|
342
392
|
|
|
343
|
-
export { type A2AServerOptions, type AdminRouterOptions, type BrowserGatewayOptions, type FileUploadOptions, type GatewayOptions, MCPManager, type MCPServerEntry, type MCPServerSummary, type RouterOptions, type SwaggerOptions, type VoiceGatewayOptions, buildMultiModalInput, createA2AServer, createAdminRouter, createAgentGateway, createAgentRouter, createBrowserGateway, createFileUploadMiddleware, 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);
|
|
@@ -2037,6 +2267,128 @@ function createAgentGateway(opts) {
|
|
|
2037
2267
|
});
|
|
2038
2268
|
}
|
|
2039
2269
|
|
|
2270
|
+
// src/socketio/vision-gateway.ts
|
|
2271
|
+
function createVisionGateway(opts) {
|
|
2272
|
+
const ns = opts.io.of(opts.namespace ?? "/radaros-vision");
|
|
2273
|
+
if (opts.authMiddleware) {
|
|
2274
|
+
ns.use(opts.authMiddleware);
|
|
2275
|
+
}
|
|
2276
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
2277
|
+
ns.on("connection", (socket) => {
|
|
2278
|
+
socket.on(
|
|
2279
|
+
"vision.start",
|
|
2280
|
+
async (data) => {
|
|
2281
|
+
const agent = opts.agents[data.agentName];
|
|
2282
|
+
if (!agent) {
|
|
2283
|
+
socket.emit("vision.error", { error: `Vision agent "${data.agentName}" not found` });
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (activeSessions.has(socket.id)) {
|
|
2287
|
+
socket.emit("vision.error", { error: "A vision session is already active for this connection" });
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
try {
|
|
2291
|
+
const apiKey = data.apiKey ?? socket.handshake?.auth?.apiKey;
|
|
2292
|
+
const userId = data.userId ?? socket.handshake?.auth?.userId;
|
|
2293
|
+
const sessionId = data.sessionId ?? socket.handshake?.auth?.sessionId;
|
|
2294
|
+
const session = await agent.connect({ apiKey, userId, sessionId });
|
|
2295
|
+
activeSessions.set(socket.id, session);
|
|
2296
|
+
session.on("audio", (ev) => {
|
|
2297
|
+
socket.emit("vision.audio", {
|
|
2298
|
+
data: ev.data.toString("base64"),
|
|
2299
|
+
mimeType: ev.mimeType ?? "audio/pcm"
|
|
2300
|
+
});
|
|
2301
|
+
});
|
|
2302
|
+
session.on("transcript", (ev) => {
|
|
2303
|
+
socket.emit("vision.transcript", { text: ev.text, role: ev.role });
|
|
2304
|
+
});
|
|
2305
|
+
session.on("text", (ev) => {
|
|
2306
|
+
socket.emit("vision.text", { text: ev.text });
|
|
2307
|
+
});
|
|
2308
|
+
session.on("tool_call_start", (ev) => {
|
|
2309
|
+
socket.emit("vision.tool.call", { name: ev.name, args: ev.args });
|
|
2310
|
+
});
|
|
2311
|
+
session.on("tool_result", (ev) => {
|
|
2312
|
+
socket.emit("vision.tool.result", { name: ev.name, result: ev.result });
|
|
2313
|
+
});
|
|
2314
|
+
session.on("usage", (ev) => {
|
|
2315
|
+
socket.emit("vision.usage", ev);
|
|
2316
|
+
});
|
|
2317
|
+
session.on("interrupted", () => {
|
|
2318
|
+
socket.emit("vision.interrupted");
|
|
2319
|
+
});
|
|
2320
|
+
session.on("error", (ev) => {
|
|
2321
|
+
socket.emit("vision.error", { error: ev.error.message });
|
|
2322
|
+
});
|
|
2323
|
+
session.on("disconnected", () => {
|
|
2324
|
+
activeSessions.delete(socket.id);
|
|
2325
|
+
socket.emit("vision.stopped");
|
|
2326
|
+
});
|
|
2327
|
+
socket.emit("vision.started", { userId });
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
socket.emit("vision.error", { error: error.message });
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
);
|
|
2333
|
+
socket.on("vision.audio", (data) => {
|
|
2334
|
+
const session = activeSessions.get(socket.id);
|
|
2335
|
+
if (!session) return;
|
|
2336
|
+
if (typeof data?.data !== "string" || data.data.length > 1e6) return;
|
|
2337
|
+
try {
|
|
2338
|
+
session.sendAudio(Buffer.from(data.data, "base64"));
|
|
2339
|
+
} catch {
|
|
2340
|
+
socket.emit("vision.error", { error: "Invalid audio data" });
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
socket.on("vision.image", (data) => {
|
|
2344
|
+
const session = activeSessions.get(socket.id);
|
|
2345
|
+
if (!session) return;
|
|
2346
|
+
if (typeof data?.data !== "string" || data.data.length > 5e6) return;
|
|
2347
|
+
try {
|
|
2348
|
+
session.sendImage(Buffer.from(data.data, "base64"), data.mimeType ?? "image/jpeg");
|
|
2349
|
+
} catch {
|
|
2350
|
+
socket.emit("vision.error", { error: "Invalid image data" });
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
socket.on("vision.text", (data) => {
|
|
2354
|
+
const session = activeSessions.get(socket.id);
|
|
2355
|
+
if (!session) return;
|
|
2356
|
+
if (typeof data?.text !== "string" || data.text.length > 1e4) return;
|
|
2357
|
+
session.sendText(data.text);
|
|
2358
|
+
});
|
|
2359
|
+
socket.on("vision.interrupt", () => {
|
|
2360
|
+
const session = activeSessions.get(socket.id);
|
|
2361
|
+
if (!session) return;
|
|
2362
|
+
session.interrupt();
|
|
2363
|
+
});
|
|
2364
|
+
socket.on("vision.stop", async () => {
|
|
2365
|
+
const session = activeSessions.get(socket.id);
|
|
2366
|
+
if (!session) return;
|
|
2367
|
+
try {
|
|
2368
|
+
await session.close();
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
console.warn("[vision-gateway] Error closing session:", err);
|
|
2371
|
+
}
|
|
2372
|
+
activeSessions.delete(socket.id);
|
|
2373
|
+
socket.emit("vision.stopped");
|
|
2374
|
+
});
|
|
2375
|
+
socket.on("disconnect", async () => {
|
|
2376
|
+
const session = activeSessions.get(socket.id);
|
|
2377
|
+
if (session) {
|
|
2378
|
+
try {
|
|
2379
|
+
await session.close();
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
console.warn(
|
|
2382
|
+
"[radaros/vision-gateway] Error closing session on disconnect:",
|
|
2383
|
+
err instanceof Error ? err.message : err
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
activeSessions.delete(socket.id);
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2040
2392
|
// src/socketio/voice-gateway.ts
|
|
2041
2393
|
function createVoiceGateway(opts) {
|
|
2042
2394
|
const ns = opts.io.of(opts.namespace ?? "/radaros-voice");
|
|
@@ -2177,6 +2529,10 @@ export {
|
|
|
2177
2529
|
createAgentRouter,
|
|
2178
2530
|
createBrowserGateway,
|
|
2179
2531
|
createFileUploadMiddleware,
|
|
2532
|
+
createGatewayRouter,
|
|
2533
|
+
createJwtMiddleware,
|
|
2534
|
+
createRbacMiddleware,
|
|
2535
|
+
createVisionGateway,
|
|
2180
2536
|
createVoiceGateway,
|
|
2181
2537
|
errorHandler,
|
|
2182
2538
|
generateAgentCard,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radaros/transport",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.48",
|
|
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.48",
|
|
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",
|