Skip to content

Commit 5413122

Browse files
committed
feat: Add JWT authentication and token-based access control
1 parent fe4f85e commit 5413122

File tree

6 files changed

+275
-189
lines changed

6 files changed

+275
-189
lines changed

.env.example

+6
Original file line numberDiff line numberDiff line change
@@ -968,3 +968,9 @@ BUNDLE_EXECUTOR_ADDRESS= # Address of the bundle executor contract
968968
DESK_EXCHANGE_PRIVATE_KEY= # Required for trading and cancelling orders
969969
DESK_EXCHANGE_NETWORK= # "mainnet" or "testnet
970970

971+
# JWT
972+
JWT_ENABLED=
973+
JWT_SECRET_KEY=
974+
JWT_EXPIRED=
975+
JWT_USERNAME=
976+
JWT_PASSWORD=

packages/client-direct/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
"dependencies": {
2222
"@elizaos/core": "workspace:*",
2323
"@elizaos/plugin-image-generation": "workspace:*",
24-
"@elizaos/plugin-tee-verifiable-log": "workspace:*",
2524
"@elizaos/plugin-tee-log": "workspace:*",
25+
"@elizaos/plugin-tee-verifiable-log": "workspace:*",
2626
"@types/body-parser": "1.19.5",
2727
"@types/cors": "2.8.17",
2828
"@types/express": "5.0.0",
2929
"body-parser": "1.20.3",
3030
"cors": "2.8.5",
3131
"discord.js": "14.16.3",
3232
"express": "4.21.1",
33+
"jsonwebtoken": "^9.0.2",
3334
"multer": "1.4.5-lts.1",
3435
"openai": "4.73.0"
3536
},

packages/client-direct/src/api.ts

+10-184
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,16 @@ import {
88
type AgentRuntime,
99
elizaLogger,
1010
getEnvVariable,
11-
type UUID,
12-
validateCharacterConfig,
1311
ServiceType,
1412
type Character,
13+
settings,
1514
} from "@elizaos/core";
1615

1716
import type { TeeLogQuery, TeeLogService } from "@elizaos/plugin-tee-log";
1817
import { REST, Routes } from "discord.js";
1918
import type { DirectClient } from ".";
20-
import { validateUuid } from "@elizaos/core";
21-
22-
interface UUIDParams {
23-
agentId: UUID;
24-
roomId?: UUID;
25-
}
26-
27-
function validateUUIDParams(
28-
params: { agentId: string; roomId?: string },
29-
res: express.Response
30-
): UUIDParams | null {
31-
const agentId = validateUuid(params.agentId);
32-
if (!agentId) {
33-
res.status(400).json({
34-
error: "Invalid AgentId format. Expected to be a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
35-
});
36-
return null;
37-
}
38-
39-
if (params.roomId) {
40-
const roomId = validateUuid(params.roomId);
41-
if (!roomId) {
42-
res.status(400).json({
43-
error: "Invalid RoomId format. Expected to be a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
44-
});
45-
return null;
46-
}
47-
return { agentId, roomId };
48-
}
49-
50-
return { agentId };
51-
}
19+
import { validateUUIDParams } from ".";
20+
import { md5, signToken } from "./auth";
5221

5322
export function createApiRouter(
5423
agents: Map<string, AgentRuntime>,
@@ -65,14 +34,6 @@ export function createApiRouter(
6534
})
6635
);
6736

68-
router.get("/", (req, res) => {
69-
res.send("Welcome, this is the REST API!");
70-
});
71-
72-
router.get("/hello", (req, res) => {
73-
res.json({ message: "Hello World!" });
74-
});
75-
7637
router.get("/agents", (req, res) => {
7738
const agentsList = Array.from(agents.values()).map((agent) => ({
7839
id: agent.agentId,
@@ -116,102 +77,6 @@ export function createApiRouter(
11677
});
11778
});
11879

119-
router.delete("/agents/:agentId", async (req, res) => {
120-
const { agentId } = validateUUIDParams(req.params, res) ?? {
121-
agentId: null,
122-
};
123-
if (!agentId) return;
124-
125-
const agent: AgentRuntime = agents.get(agentId);
126-
127-
if (agent) {
128-
agent.stop();
129-
directClient.unregisterAgent(agent);
130-
res.status(204).json({ success: true });
131-
} else {
132-
res.status(404).json({ error: "Agent not found" });
133-
}
134-
});
135-
136-
router.post("/agents/:agentId/set", async (req, res) => {
137-
const { agentId } = validateUUIDParams(req.params, res) ?? {
138-
agentId: null,
139-
};
140-
if (!agentId) return;
141-
142-
let agent: AgentRuntime = agents.get(agentId);
143-
144-
// update character
145-
if (agent) {
146-
// stop agent
147-
agent.stop();
148-
directClient.unregisterAgent(agent);
149-
// if it has a different name, the agentId will change
150-
}
151-
152-
// stores the json data before it is modified with added data
153-
const characterJson = { ...req.body };
154-
155-
// load character from body
156-
const character = req.body;
157-
try {
158-
validateCharacterConfig(character);
159-
} catch (e) {
160-
elizaLogger.error(`Error parsing character: ${e}`);
161-
res.status(400).json({
162-
success: false,
163-
message: e.message,
164-
});
165-
return;
166-
}
167-
168-
// start it up (and register it)
169-
try {
170-
agent = await directClient.startAgent(character);
171-
elizaLogger.log(`${character.name} started`);
172-
} catch (e) {
173-
elizaLogger.error(`Error starting agent: ${e}`);
174-
res.status(500).json({
175-
success: false,
176-
message: e.message,
177-
});
178-
return;
179-
}
180-
181-
if (process.env.USE_CHARACTER_STORAGE === "true") {
182-
try {
183-
const filename = `${agent.agentId}.json`;
184-
const uploadDir = path.join(
185-
process.cwd(),
186-
"data",
187-
"characters"
188-
);
189-
const filepath = path.join(uploadDir, filename);
190-
await fs.promises.mkdir(uploadDir, { recursive: true });
191-
await fs.promises.writeFile(
192-
filepath,
193-
JSON.stringify(
194-
{ ...characterJson, id: agent.agentId },
195-
null,
196-
2
197-
)
198-
);
199-
elizaLogger.info(
200-
`Character stored successfully at ${filepath}`
201-
);
202-
} catch (error) {
203-
elizaLogger.error(
204-
`Failed to store character: ${error.message}`
205-
);
206-
}
207-
}
208-
209-
res.json({
210-
id: character.id,
211-
character: character,
212-
});
213-
});
214-
21580
router.get("/agents/:agentId/channels", async (req, res) => {
21681
const { agentId } = validateUUIDParams(req.params, res) ?? {
21782
agentId: null,
@@ -404,53 +269,14 @@ export function createApiRouter(
404269
}
405270
);
406271

407-
router.post("/agent/start", async (req, res) => {
408-
const { characterPath, characterJson } = req.body;
409-
console.log("characterPath:", characterPath);
410-
console.log("characterJson:", characterJson);
411-
try {
412-
let character: Character;
413-
if (characterJson) {
414-
character = await directClient.jsonToCharacter(
415-
characterPath,
416-
characterJson
417-
);
418-
} else if (characterPath) {
419-
character =
420-
await directClient.loadCharacterTryPath(characterPath);
421-
} else {
422-
throw new Error("No character path or JSON provided");
423-
}
424-
await directClient.startAgent(character);
425-
elizaLogger.log(`${character.name} started`);
426-
427-
res.json({
428-
id: character.id,
429-
character: character,
430-
});
431-
} catch (e) {
432-
elizaLogger.error(`Error parsing character: ${e}`);
433-
res.status(400).json({
434-
error: e.message,
435-
});
436-
return;
437-
}
438-
});
439-
440-
router.post("/agents/:agentId/stop", async (req, res) => {
441-
const agentId = req.params.agentId;
442-
console.log("agentId", agentId);
443-
const agent: AgentRuntime = agents.get(agentId);
444-
445-
// update character
446-
if (agent) {
447-
// stop agent
448-
agent.stop();
449-
directClient.unregisterAgent(agent);
450-
// if it has a different name, the agentId will change
451-
res.json({ success: true });
272+
router.post("/auth/login", async (req, res) => {
273+
const { username, password } = req.body;
274+
const valid = username === settings.JWT_USERNAME && password === md5(settings.JWT_PASSWORD);
275+
if (valid) {
276+
const token = signToken({ username });
277+
res.json({ success: true, token: token });
452278
} else {
453-
res.status(404).json({ error: "Agent not found" });
279+
res.status(401).json({ error: "Invalid username or password" });
454280
}
455281
});
456282

packages/client-direct/src/auth.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import jwt from 'jsonwebtoken';
2+
import { v4 as uuidv4 } from "uuid";
3+
import { settings } from "@elizaos/core";
4+
import crypto from 'crypto';
5+
6+
export function md5(text: any) {
7+
return crypto.createHash('md5').update(text).digest('hex');
8+
}
9+
10+
export const signToken = (data: Record<string, any>, expiresIn: string | number = settings.JWT_EXPIRED): string => {
11+
const _salt = uuidv4();
12+
return jwt.sign({ ...data, _salt }, settings.JWT_SECRET_KEY, {
13+
expiresIn: expiresIn
14+
});
15+
};
16+
17+
export const verifyToken = (authorization: string): Promise<any> => {
18+
return new Promise((resolve, reject) => {
19+
jwt.verify(authorization, settings.JWT_SECRET_KEY, async (err: any, decode: any) => {
20+
if (err) {
21+
reject(err);
22+
} else {
23+
resolve(decode);
24+
}
25+
});
26+
});
27+
};
28+

packages/client-direct/src/index.ts

+72
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ import {
2222
settings,
2323
type IAgentRuntime,
2424
type TypeDatabaseAdapter,
25+
type UUID,
26+
validateUuid,
2527
} from "@elizaos/core";
2628
import { createApiRouter } from "./api.ts";
2729
import * as fs from "fs";
2830
import * as path from "path";
2931
import { createVerifiableLogApiRouter } from "./verifiable-log-api.ts";
3032
import { createManageApiRouter } from "./manage-api.ts";
33+
import { verifyToken } from "./auth.ts";
3134
import OpenAI from "openai";
3235

3336
const storage = multer.diskStorage({
@@ -110,6 +113,64 @@ Response format should be formatted in a JSON block like this:
110113
\`\`\`
111114
`;
112115

116+
async function verifyTokenMiddleware(req: any, res: any, next) {
117+
// if JWT is not enabled, skip verification
118+
if (!(settings.JWT_ENABLED && settings.JWT_ENABLED.toLowerCase() === 'true')) {
119+
next();
120+
return;
121+
}
122+
123+
const url: string = req.url.split('?')[0];
124+
if (url.indexOf('/manage/') !== 0) {
125+
next();
126+
} else {
127+
try {
128+
const { authorization } = req.headers;
129+
if (!authorization) throw new Error('no token');
130+
const token = await verifyToken(authorization.split(' ')[1]);
131+
if (token) {
132+
next();
133+
} else {
134+
throw new Error('fail to verify token');
135+
}
136+
} catch (err: any) {
137+
res.status(401).json({ error: err.message });
138+
return;
139+
}
140+
}
141+
};
142+
143+
interface UUIDParams {
144+
agentId: UUID;
145+
roomId?: UUID;
146+
}
147+
148+
export function validateUUIDParams(
149+
params: { agentId: string; roomId?: string },
150+
res: express.Response
151+
): UUIDParams | null {
152+
const agentId = validateUuid(params.agentId);
153+
if (!agentId) {
154+
res.status(400).json({
155+
error: "Invalid AgentId format. Expected to be a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
156+
});
157+
return null;
158+
}
159+
160+
if (params.roomId) {
161+
const roomId = validateUuid(params.roomId);
162+
if (!roomId) {
163+
res.status(400).json({
164+
error: "Invalid RoomId format. Expected to be a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
165+
});
166+
return null;
167+
}
168+
return { agentId, roomId };
169+
}
170+
171+
return { agentId };
172+
}
173+
113174
export class DirectClient {
114175
public app: express.Application;
115176
private agents: Map<string, AgentRuntime>; // container management
@@ -128,6 +189,17 @@ export class DirectClient {
128189
this.app.use(bodyParser.json());
129190
this.app.use(bodyParser.urlencoded({ extended: true }));
130191

192+
this.app.use(verifyTokenMiddleware);
193+
194+
195+
this.app.get("/", (req, res) => {
196+
res.send("Welcome, this is the REST API!");
197+
});
198+
199+
this.app.get("/hello", (req, res) => {
200+
res.json({ message: "Hello World!" });
201+
});
202+
131203
// Serve both uploads and generated images
132204
this.app.use(
133205
"/media/uploads",

0 commit comments

Comments
 (0)