@radaros/transport 0.3.10 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.js +155 -11
- package/package.json +18 -3
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @radaros/transport
|
|
2
|
+
|
|
3
|
+
HTTP and WebSocket transport layer for deploying RadarOS agents as APIs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @radaros/transport
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import express from "express";
|
|
15
|
+
import { Agent, openai } from "@radaros/core";
|
|
16
|
+
import { createAgentRouter } from "@radaros/transport";
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
const agent = new Agent({
|
|
22
|
+
name: "assistant",
|
|
23
|
+
model: openai("gpt-4o"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.use("/api", createAgentRouter({ agents: { assistant: agent } }));
|
|
27
|
+
app.listen(3000);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Express Router** — REST API with streaming support
|
|
33
|
+
- **Socket.IO Gateway** — Real-time WebSocket communication
|
|
34
|
+
- **A2A Server** — Agent-to-Agent protocol support
|
|
35
|
+
- **CORS & Rate Limiting** — Built-in security middleware
|
|
36
|
+
- **Swagger** — Auto-generated API documentation
|
|
37
|
+
- **File Upload** — Multipart form data support
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
Full docs at [radaros.mintlify.dev](https://radaros.mintlify.dev)
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -42,8 +42,12 @@ interface FileUploadOptions {
|
|
|
42
42
|
declare function createFileUploadMiddleware(opts?: FileUploadOptions): any;
|
|
43
43
|
declare function buildMultiModalInput(body: any, files?: any[]): string | any[];
|
|
44
44
|
|
|
45
|
-
declare function errorHandler(
|
|
46
|
-
|
|
45
|
+
declare function errorHandler(options?: {
|
|
46
|
+
logger?: Pick<Console, "error">;
|
|
47
|
+
}): (err: any, _req: any, res: any, _next: any) => void;
|
|
48
|
+
declare function requestLogger(options?: {
|
|
49
|
+
logger?: Pick<Console, "log">;
|
|
50
|
+
}): (req: any, _res: any, next: any) => void;
|
|
47
51
|
|
|
48
52
|
interface SwaggerOptions {
|
|
49
53
|
/** Enable Swagger UI at /docs. Default: false */
|
|
@@ -75,6 +79,13 @@ interface RouterOptions {
|
|
|
75
79
|
swagger?: SwaggerOptions;
|
|
76
80
|
/** File upload configuration for multi-modal inputs */
|
|
77
81
|
fileUpload?: boolean | FileUploadOptions;
|
|
82
|
+
/** CORS configuration. Pass true or '*' for permissive, a string for a single origin, or an array for multiple origins. */
|
|
83
|
+
cors?: string | string[] | boolean;
|
|
84
|
+
/** Rate limiting configuration. Pass true for defaults (100 req/min), or an object to customize. */
|
|
85
|
+
rateLimit?: {
|
|
86
|
+
windowMs?: number;
|
|
87
|
+
max?: number;
|
|
88
|
+
} | boolean;
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
declare function createAgentRouter(opts: RouterOptions): any;
|
|
@@ -169,6 +180,8 @@ interface GatewayOptions {
|
|
|
169
180
|
io: any;
|
|
170
181
|
namespace?: string;
|
|
171
182
|
authMiddleware?: (socket: any, next: (err?: Error) => void) => void;
|
|
183
|
+
/** Max requests per minute per socket. Default: 60 */
|
|
184
|
+
maxRequestsPerMinute?: number;
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
declare function createAgentGateway(opts: GatewayOptions): void;
|
package/dist/index.js
CHANGED
|
@@ -64,10 +64,19 @@ function generateMultiAgentCard(agents, serverUrl, provider, version) {
|
|
|
64
64
|
var _require = createRequire(import.meta.url);
|
|
65
65
|
var TaskStore = class {
|
|
66
66
|
tasks = /* @__PURE__ */ new Map();
|
|
67
|
+
maxTasks = 1e4;
|
|
67
68
|
get(id) {
|
|
68
69
|
return this.tasks.get(id);
|
|
69
70
|
}
|
|
70
71
|
set(task) {
|
|
72
|
+
if (this.tasks.size >= this.maxTasks) {
|
|
73
|
+
for (const [id, t] of this.tasks) {
|
|
74
|
+
if (t.status?.state === "completed" || t.status?.state === "canceled") {
|
|
75
|
+
this.tasks.delete(id);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
71
80
|
this.tasks.set(task.id, task);
|
|
72
81
|
}
|
|
73
82
|
updateState(id, state, message) {
|
|
@@ -403,17 +412,19 @@ function buildMultiModalInput(body, files) {
|
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
// src/express/middleware.ts
|
|
406
|
-
function errorHandler() {
|
|
415
|
+
function errorHandler(options) {
|
|
416
|
+
const log = options?.logger ?? console;
|
|
407
417
|
return (err, _req, res, _next) => {
|
|
408
|
-
|
|
418
|
+
log.error("[radaros:transport] Error:", err.message);
|
|
409
419
|
res.status(err.statusCode ?? 500).json({
|
|
410
420
|
error: err.message ?? "Internal server error"
|
|
411
421
|
});
|
|
412
422
|
};
|
|
413
423
|
}
|
|
414
|
-
function requestLogger() {
|
|
424
|
+
function requestLogger(options) {
|
|
425
|
+
const log = options?.logger ?? console;
|
|
415
426
|
return (req, _res, next) => {
|
|
416
|
-
|
|
427
|
+
log.log(`[radaros:transport] ${req.method} ${req.path}`);
|
|
417
428
|
next();
|
|
418
429
|
};
|
|
419
430
|
}
|
|
@@ -864,12 +875,76 @@ function serveSwaggerUI(spec) {
|
|
|
864
875
|
|
|
865
876
|
// src/express/router-factory.ts
|
|
866
877
|
var _require4 = createRequire4(import.meta.url);
|
|
878
|
+
function corsMiddleware(origins) {
|
|
879
|
+
return (req, res, next) => {
|
|
880
|
+
const origin = req.headers.origin;
|
|
881
|
+
let allowed = false;
|
|
882
|
+
if (origins === true || origins === "*") {
|
|
883
|
+
allowed = true;
|
|
884
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
885
|
+
} else if (typeof origins === "string") {
|
|
886
|
+
allowed = origin === origins;
|
|
887
|
+
if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
|
|
888
|
+
} else if (Array.isArray(origins)) {
|
|
889
|
+
allowed = origins.includes(origin);
|
|
890
|
+
if (allowed) res.setHeader("Access-Control-Allow-Origin", origin);
|
|
891
|
+
}
|
|
892
|
+
if (allowed) {
|
|
893
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
894
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
|
|
895
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
896
|
+
}
|
|
897
|
+
if (req.method === "OPTIONS") {
|
|
898
|
+
res.status(204).end();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
next();
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function rateLimitMiddleware(config = {}) {
|
|
905
|
+
const windowMs = config.windowMs ?? 6e4;
|
|
906
|
+
const max = config.max ?? 100;
|
|
907
|
+
const hits = /* @__PURE__ */ new Map();
|
|
908
|
+
return (req, res, next) => {
|
|
909
|
+
const key = req.ip ?? req.socket?.remoteAddress ?? "unknown";
|
|
910
|
+
const now = Date.now();
|
|
911
|
+
const record = hits.get(key);
|
|
912
|
+
if (!record || now > record.resetTime) {
|
|
913
|
+
hits.set(key, { count: 1, resetTime: now + windowMs });
|
|
914
|
+
next();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
record.count++;
|
|
918
|
+
if (record.count > max) {
|
|
919
|
+
res.status(429).json({ error: "Too many requests, please try again later" });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
next();
|
|
923
|
+
};
|
|
924
|
+
}
|
|
867
925
|
var API_KEY_HEADERS = {
|
|
868
926
|
"x-openai-api-key": "openai",
|
|
869
927
|
"x-google-api-key": "google",
|
|
870
928
|
"x-anthropic-api-key": "anthropic",
|
|
871
929
|
"x-api-key": "_generic"
|
|
872
930
|
};
|
|
931
|
+
function validateBody(body, fields) {
|
|
932
|
+
if (!body || typeof body !== "object") throw new Error("Invalid request body");
|
|
933
|
+
const result = {};
|
|
934
|
+
for (const [key, type] of Object.entries(fields)) {
|
|
935
|
+
const val = body[key];
|
|
936
|
+
const isOptional = type.endsWith("?");
|
|
937
|
+
const baseType = type.replace("?", "");
|
|
938
|
+
if (val === void 0 || val === null) {
|
|
939
|
+
if (!isOptional) throw new Error(`Missing required field: ${key}`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (baseType === "string" && typeof val !== "string") throw new Error(`Field ${key} must be a string`);
|
|
943
|
+
if (baseType === "object" && typeof val !== "object") throw new Error(`Field ${key} must be an object`);
|
|
944
|
+
result[key] = val;
|
|
945
|
+
}
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
873
948
|
function extractApiKey(req, agent) {
|
|
874
949
|
for (const [header, provider] of Object.entries(API_KEY_HEADERS)) {
|
|
875
950
|
const value = req.headers[header];
|
|
@@ -887,6 +962,13 @@ function createAgentRouter(opts) {
|
|
|
887
962
|
throw new Error("express is required for createAgentRouter. Install it: npm install express");
|
|
888
963
|
}
|
|
889
964
|
const router = express.Router();
|
|
965
|
+
if (opts.cors) {
|
|
966
|
+
router.use(corsMiddleware(opts.cors));
|
|
967
|
+
}
|
|
968
|
+
if (opts.rateLimit) {
|
|
969
|
+
const config = opts.rateLimit === true ? {} : opts.rateLimit;
|
|
970
|
+
router.use(rateLimitMiddleware(config));
|
|
971
|
+
}
|
|
890
972
|
if (opts.middleware) {
|
|
891
973
|
for (const mw of opts.middleware) {
|
|
892
974
|
router.use(mw);
|
|
@@ -928,25 +1010,38 @@ function createAgentRouter(opts) {
|
|
|
928
1010
|
`/agents/${name}/run`,
|
|
929
1011
|
withUpload(async (req, res) => {
|
|
930
1012
|
try {
|
|
931
|
-
const
|
|
1013
|
+
const validated = validateBody(req.body, {
|
|
1014
|
+
input: "string",
|
|
1015
|
+
sessionId: "string?",
|
|
1016
|
+
userId: "string?"
|
|
1017
|
+
});
|
|
1018
|
+
const input = buildMultiModalInput(req.body, req.files) ?? validated.input;
|
|
932
1019
|
if (!input) {
|
|
933
1020
|
return res.status(400).json({ error: "input is required" });
|
|
934
1021
|
}
|
|
935
|
-
const
|
|
1022
|
+
const sessionId = validated.sessionId;
|
|
1023
|
+
const userId = validated.userId;
|
|
936
1024
|
const apiKey = extractApiKey(req, agent);
|
|
937
1025
|
const result = await agent.run(input, { sessionId, userId, apiKey });
|
|
938
1026
|
res.json(result);
|
|
939
1027
|
} catch (error) {
|
|
940
|
-
res.status(
|
|
1028
|
+
res.status(400).json({ error: error.message });
|
|
941
1029
|
}
|
|
942
1030
|
})
|
|
943
1031
|
);
|
|
944
1032
|
router.post(`/agents/${name}/stream`, async (req, res) => {
|
|
945
1033
|
try {
|
|
946
|
-
const
|
|
1034
|
+
const validated = validateBody(req.body, {
|
|
1035
|
+
input: "string",
|
|
1036
|
+
sessionId: "string?",
|
|
1037
|
+
userId: "string?"
|
|
1038
|
+
});
|
|
1039
|
+
const input = validated.input;
|
|
947
1040
|
if (!input) {
|
|
948
1041
|
return res.status(400).json({ error: "input is required" });
|
|
949
1042
|
}
|
|
1043
|
+
const sessionId = validated.sessionId;
|
|
1044
|
+
const userId = validated.userId;
|
|
950
1045
|
const apiKey = extractApiKey(req, agent);
|
|
951
1046
|
res.writeHead(200, {
|
|
952
1047
|
"Content-Type": "text/event-stream",
|
|
@@ -978,10 +1073,17 @@ function createAgentRouter(opts) {
|
|
|
978
1073
|
for (const [name, team] of Object.entries(opts.teams)) {
|
|
979
1074
|
router.post(`/teams/${name}/run`, async (req, res) => {
|
|
980
1075
|
try {
|
|
981
|
-
const
|
|
1076
|
+
const validated = validateBody(req.body, {
|
|
1077
|
+
input: "string",
|
|
1078
|
+
sessionId: "string?",
|
|
1079
|
+
userId: "string?"
|
|
1080
|
+
});
|
|
1081
|
+
const input = validated.input;
|
|
982
1082
|
if (!input) {
|
|
983
1083
|
return res.status(400).json({ error: "input is required" });
|
|
984
1084
|
}
|
|
1085
|
+
const sessionId = validated.sessionId;
|
|
1086
|
+
const userId = validated.userId;
|
|
985
1087
|
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
986
1088
|
const result = await team.run(input, { sessionId, userId, apiKey });
|
|
987
1089
|
res.json(result);
|
|
@@ -991,10 +1093,17 @@ function createAgentRouter(opts) {
|
|
|
991
1093
|
});
|
|
992
1094
|
router.post(`/teams/${name}/stream`, async (req, res) => {
|
|
993
1095
|
try {
|
|
994
|
-
const
|
|
1096
|
+
const validated = validateBody(req.body, {
|
|
1097
|
+
input: "string",
|
|
1098
|
+
sessionId: "string?",
|
|
1099
|
+
userId: "string?"
|
|
1100
|
+
});
|
|
1101
|
+
const input = validated.input;
|
|
995
1102
|
if (!input) {
|
|
996
1103
|
return res.status(400).json({ error: "input is required" });
|
|
997
1104
|
}
|
|
1105
|
+
const sessionId = validated.sessionId;
|
|
1106
|
+
const userId = validated.userId;
|
|
998
1107
|
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
999
1108
|
res.writeHead(200, {
|
|
1000
1109
|
"Content-Type": "text/event-stream",
|
|
@@ -1152,13 +1261,42 @@ function createBrowserGateway(opts) {
|
|
|
1152
1261
|
}
|
|
1153
1262
|
|
|
1154
1263
|
// src/socketio/gateway.ts
|
|
1264
|
+
function createSocketRateLimiter(maxPerMinute = 60) {
|
|
1265
|
+
return () => {
|
|
1266
|
+
let count = 0;
|
|
1267
|
+
let resetTime = Date.now() + 6e4;
|
|
1268
|
+
return () => {
|
|
1269
|
+
const now = Date.now();
|
|
1270
|
+
if (now > resetTime) {
|
|
1271
|
+
count = 0;
|
|
1272
|
+
resetTime = now + 6e4;
|
|
1273
|
+
}
|
|
1274
|
+
count++;
|
|
1275
|
+
return count <= maxPerMinute;
|
|
1276
|
+
};
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1155
1279
|
function createAgentGateway(opts) {
|
|
1156
1280
|
const ns = opts.io.of(opts.namespace ?? "/radaros");
|
|
1157
1281
|
if (opts.authMiddleware) {
|
|
1158
1282
|
ns.use(opts.authMiddleware);
|
|
1159
1283
|
}
|
|
1284
|
+
const rateLimiterFactory = createSocketRateLimiter(opts.maxRequestsPerMinute ?? 60);
|
|
1160
1285
|
ns.on("connection", (socket) => {
|
|
1286
|
+
const checkRate = rateLimiterFactory();
|
|
1161
1287
|
socket.on("agent.run", async (data) => {
|
|
1288
|
+
if (!checkRate()) {
|
|
1289
|
+
socket.emit("agent.error", { error: "Rate limit exceeded" });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (!data || typeof data.input !== "string" || !data.input.trim()) {
|
|
1293
|
+
socket.emit("agent.error", { error: "Invalid input: must be a non-empty string" });
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (data.sessionId !== void 0 && typeof data.sessionId !== "string") {
|
|
1297
|
+
socket.emit("agent.error", { error: "Invalid sessionId: must be a string" });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1162
1300
|
const agent = opts.agents?.[data.name];
|
|
1163
1301
|
if (!agent) {
|
|
1164
1302
|
socket.emit("agent.error", {
|
|
@@ -1191,6 +1329,8 @@ function createAgentGateway(opts) {
|
|
|
1191
1329
|
socket.emit("agent.error", { error: error.message });
|
|
1192
1330
|
}
|
|
1193
1331
|
});
|
|
1332
|
+
socket.on("disconnect", () => {
|
|
1333
|
+
});
|
|
1194
1334
|
socket.on("team.run", async (data) => {
|
|
1195
1335
|
const team = opts.teams?.[data.name];
|
|
1196
1336
|
if (!team) {
|
|
@@ -1317,7 +1457,11 @@ function createVoiceGateway(opts) {
|
|
|
1317
1457
|
if (session) {
|
|
1318
1458
|
try {
|
|
1319
1459
|
await session.close();
|
|
1320
|
-
} catch {
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
console.warn(
|
|
1462
|
+
"[radaros/voice-gateway] Error closing session on disconnect:",
|
|
1463
|
+
err instanceof Error ? err.message : err
|
|
1464
|
+
);
|
|
1321
1465
|
}
|
|
1322
1466
|
activeSessions.delete(socket.id);
|
|
1323
1467
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radaros/transport",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
|
+
"description": "HTTP and WebSocket transport layer for RadarOS agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/xhipment/agentOs.git",
|
|
9
|
+
"directory": "packages/transport"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"agents",
|
|
14
|
+
"express",
|
|
15
|
+
"socketio",
|
|
16
|
+
"api",
|
|
17
|
+
"transport"
|
|
18
|
+
],
|
|
4
19
|
"type": "module",
|
|
5
20
|
"main": "./dist/index.js",
|
|
6
21
|
"types": "./dist/index.d.ts",
|
|
@@ -24,10 +39,10 @@
|
|
|
24
39
|
"typescript": "^5.6.0"
|
|
25
40
|
},
|
|
26
41
|
"peerDependencies": {
|
|
27
|
-
"@radaros/core": "^0.3.
|
|
42
|
+
"@radaros/core": "^0.3.13",
|
|
28
43
|
"@types/express": "^4.0.0 || ^5.0.0",
|
|
29
44
|
"express": "^4.0.0 || ^5.0.0",
|
|
30
|
-
"multer": ">=
|
|
45
|
+
"multer": ">=2.0.0",
|
|
31
46
|
"socket.io": "^4.0.0",
|
|
32
47
|
"swagger-ui-express": "^5.0.0"
|
|
33
48
|
},
|