@slashfi/agents-sdk 0.1.0

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/src/auth.ts ADDED
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Auth Agent
3
+ *
4
+ * Built-in agent that provides OAuth2 client_credentials authentication.
5
+ * Register it into any agent registry to enable auth.
6
+ *
7
+ * Features:
8
+ * - Client credentials management (create, rotate, revoke)
9
+ * - OAuth2 client_credentials token exchange
10
+ * - JWT access tokens with scopes
11
+ * - Pluggable AuthStore interface (in-memory default)
12
+ * - Root key for admin operations
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { createAgentRegistry, createAgentServer, createAuthAgent } from '@slashfi/agents-sdk';
17
+ *
18
+ * const registry = createAgentRegistry();
19
+ * registry.register(createAuthAgent({ rootKey: process.env.ROOT_KEY }));
20
+ * registry.register(myAgent);
21
+ *
22
+ * const server = createAgentServer(registry, { port: 3000 });
23
+ * await server.start();
24
+ * ```
25
+ */
26
+
27
+ import { defineAgent, defineTool } from "./define.js";
28
+ import type { AgentDefinition, ToolContext, ToolDefinition } from "./types.js";
29
+
30
+ // ============================================
31
+ // Auth Types
32
+ // ============================================
33
+
34
+ /** Registered client */
35
+ export interface AuthClient {
36
+ clientId: string;
37
+ clientSecretHash: string;
38
+ name: string;
39
+ scopes: string[];
40
+ createdAt: number;
41
+ /** If true, this client was created via self-registration */
42
+ selfRegistered?: boolean;
43
+ }
44
+
45
+ /** Issued token metadata */
46
+ export interface AuthToken {
47
+ token: string;
48
+ clientId: string;
49
+ scopes: string[];
50
+ expiresAt: number;
51
+ issuedAt: number;
52
+ }
53
+
54
+ /** Resolved identity from a token or root key */
55
+ export interface AuthIdentity {
56
+ clientId: string;
57
+ name: string;
58
+ scopes: string[];
59
+ isRoot: boolean;
60
+ }
61
+
62
+ // ============================================
63
+ // Auth Store Interface
64
+ // ============================================
65
+
66
+ /**
67
+ * Pluggable storage for auth state.
68
+ * Implement this interface to use Postgres, Redis, SQLite, etc.
69
+ */
70
+ export interface AuthStore {
71
+ /** Create a new client. Returns the raw (unhashed) secret. */
72
+ createClient(
73
+ name: string,
74
+ scopes: string[],
75
+ selfRegistered?: boolean,
76
+ ): Promise<{ clientId: string; clientSecret: string }>;
77
+
78
+ /** Validate client credentials. Returns client if valid, null otherwise. */
79
+ validateClient(
80
+ clientId: string,
81
+ clientSecret: string,
82
+ ): Promise<AuthClient | null>;
83
+
84
+ /** Get client by ID. */
85
+ getClient(clientId: string): Promise<AuthClient | null>;
86
+
87
+ /** List all clients. */
88
+ listClients(): Promise<AuthClient[]>;
89
+
90
+ /** Revoke a client (delete). */
91
+ revokeClient(clientId: string): Promise<boolean>;
92
+
93
+ /** Rotate a client's secret. Returns new raw secret. */
94
+ rotateSecret(clientId: string): Promise<{ clientSecret: string } | null>;
95
+
96
+ /** Store a token. */
97
+ storeToken(token: AuthToken): Promise<void>;
98
+
99
+ /** Validate and retrieve a token. Returns null if invalid/expired. */
100
+ validateToken(tokenString: string): Promise<AuthToken | null>;
101
+
102
+ /** Revoke a specific token. */
103
+ revokeToken(tokenString: string): Promise<boolean>;
104
+ }
105
+
106
+ // ============================================
107
+ // In-Memory Auth Store
108
+ // ============================================
109
+
110
+ function generateId(prefix: string): string {
111
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
112
+ let id = prefix;
113
+ for (let i = 0; i < 24; i++) {
114
+ id += chars[Math.floor(Math.random() * chars.length)];
115
+ }
116
+ return id;
117
+ }
118
+
119
+ function generateSecret(): string {
120
+ const chars =
121
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
122
+ let secret = "sk_";
123
+ for (let i = 0; i < 40; i++) {
124
+ secret += chars[Math.floor(Math.random() * chars.length)];
125
+ }
126
+ return secret;
127
+ }
128
+
129
+ function generateToken(): string {
130
+ const chars =
131
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
132
+ let token = "at_";
133
+ for (let i = 0; i < 48; i++) {
134
+ token += chars[Math.floor(Math.random() * chars.length)];
135
+ }
136
+ return token;
137
+ }
138
+
139
+ /** Simple hash for storing secrets (not for production - use bcrypt/argon2) */
140
+ async function hashSecret(secret: string): Promise<string> {
141
+ const encoder = new TextEncoder();
142
+ const data = encoder.encode(secret);
143
+ const hash = await crypto.subtle.digest("SHA-256", data);
144
+ return Array.from(new Uint8Array(hash))
145
+ .map((b) => b.toString(16).padStart(2, "0"))
146
+ .join("");
147
+ }
148
+
149
+ /**
150
+ * Create an in-memory auth store.
151
+ * Suitable for development and testing. Use a persistent store for production.
152
+ */
153
+ export function createMemoryAuthStore(): AuthStore {
154
+ const clients = new Map<string, AuthClient>();
155
+ const tokens = new Map<string, AuthToken>();
156
+
157
+ return {
158
+ async createClient(name, scopes, selfRegistered) {
159
+ const clientId = generateId("ag_");
160
+ const clientSecret = generateSecret();
161
+ const secretHash = await hashSecret(clientSecret);
162
+
163
+ clients.set(clientId, {
164
+ clientId,
165
+ clientSecretHash: secretHash,
166
+ name,
167
+ scopes,
168
+ createdAt: Date.now(),
169
+ selfRegistered,
170
+ });
171
+
172
+ return { clientId, clientSecret };
173
+ },
174
+
175
+ async validateClient(clientId, clientSecret) {
176
+ const client = clients.get(clientId);
177
+ if (!client) return null;
178
+ const hash = await hashSecret(clientSecret);
179
+ return hash === client.clientSecretHash ? client : null;
180
+ },
181
+
182
+ async getClient(clientId) {
183
+ return clients.get(clientId) ?? null;
184
+ },
185
+
186
+ async listClients() {
187
+ return Array.from(clients.values());
188
+ },
189
+
190
+ async revokeClient(clientId) {
191
+ // Also revoke all tokens for this client
192
+ for (const [tokenStr, token] of tokens) {
193
+ if (token.clientId === clientId) {
194
+ tokens.delete(tokenStr);
195
+ }
196
+ }
197
+ return clients.delete(clientId);
198
+ },
199
+
200
+ async rotateSecret(clientId) {
201
+ const client = clients.get(clientId);
202
+ if (!client) return null;
203
+ const clientSecret = generateSecret();
204
+ client.clientSecretHash = await hashSecret(clientSecret);
205
+ return { clientSecret };
206
+ },
207
+
208
+ async storeToken(token) {
209
+ tokens.set(token.token, token);
210
+ },
211
+
212
+ async validateToken(tokenString) {
213
+ const token = tokens.get(tokenString);
214
+ if (!token) return null;
215
+ if (Date.now() > token.expiresAt) {
216
+ tokens.delete(tokenString);
217
+ return null;
218
+ }
219
+ return token;
220
+ },
221
+
222
+ async revokeToken(tokenString) {
223
+ return tokens.delete(tokenString);
224
+ },
225
+ };
226
+ }
227
+
228
+ // ============================================
229
+ // Auth Agent Options
230
+ // ============================================
231
+
232
+ export interface CreateAuthAgentOptions {
233
+ /** Root key for admin operations. Required. */
234
+ rootKey: string;
235
+
236
+ /** Allow self-registration via public `register` tool. Default: false */
237
+ allowRegistration?: boolean;
238
+
239
+ /** Max scopes that self-registered clients can request. Default: [] (no limit) */
240
+ registrationScopes?: string[];
241
+
242
+ /** Token TTL in seconds. Default: 3600 (1 hour) */
243
+ tokenTtl?: number;
244
+
245
+ /** Custom auth store. Default: in-memory */
246
+ store?: AuthStore;
247
+ }
248
+
249
+ // ============================================
250
+ // Create Auth Agent
251
+ // ============================================
252
+
253
+ /**
254
+ * Create the built-in `@auth` agent.
255
+ *
256
+ * Provides OAuth2 client_credentials authentication as agent tools.
257
+ * The server auto-detects this agent and wires up token validation.
258
+ */
259
+ export function createAuthAgent(
260
+ options: CreateAuthAgentOptions,
261
+ ): AgentDefinition & {
262
+ __authStore: AuthStore;
263
+ __rootKey: string;
264
+ __tokenTtl: number;
265
+ } {
266
+ const {
267
+ rootKey,
268
+ allowRegistration = false,
269
+ registrationScopes,
270
+ tokenTtl = 3600,
271
+ store = createMemoryAuthStore(),
272
+ } = options;
273
+
274
+ // --- Public Tools ---
275
+
276
+ const tokenTool = defineTool({
277
+ name: "token",
278
+ description:
279
+ "Exchange client credentials for an access token (OAuth2 client_credentials grant)",
280
+ visibility: "public",
281
+ inputSchema: {
282
+ type: "object",
283
+ properties: {
284
+ grantType: {
285
+ type: "string",
286
+ enum: ["client_credentials"],
287
+ description: "Grant type. Must be 'client_credentials'.",
288
+ },
289
+ clientId: { type: "string", description: "Client ID" },
290
+ clientSecret: { type: "string", description: "Client secret" },
291
+ },
292
+ required: ["grantType", "clientId", "clientSecret"],
293
+ },
294
+ execute: async (input: {
295
+ grantType: string;
296
+ clientId: string;
297
+ clientSecret: string;
298
+ }) => {
299
+ if (input.grantType !== "client_credentials") {
300
+ throw new Error("Unsupported grant type. Use 'client_credentials'.");
301
+ }
302
+
303
+ const client = await store.validateClient(
304
+ input.clientId,
305
+ input.clientSecret,
306
+ );
307
+ if (!client) {
308
+ throw new Error("Invalid client credentials");
309
+ }
310
+
311
+ const token: AuthToken = {
312
+ token: generateToken(),
313
+ clientId: client.clientId,
314
+ scopes: client.scopes,
315
+ issuedAt: Date.now(),
316
+ expiresAt: Date.now() + tokenTtl * 1000,
317
+ };
318
+
319
+ await store.storeToken(token);
320
+
321
+ return {
322
+ accessToken: token.token,
323
+ tokenType: "bearer",
324
+ expiresIn: tokenTtl,
325
+ scopes: client.scopes,
326
+ };
327
+ },
328
+ });
329
+
330
+ const whoamiTool = defineTool({
331
+ name: "whoami",
332
+ description: "Introspect the current authentication context",
333
+ visibility: "public",
334
+ inputSchema: { type: "object", properties: {} },
335
+ execute: async (_input: unknown, ctx: ToolContext) => {
336
+ return {
337
+ callerId: ctx.callerId,
338
+ callerType: ctx.callerType,
339
+ scopes: (ctx as ToolContext & { scopes?: string[] }).scopes ?? [],
340
+ isRoot: ctx.callerId === "root",
341
+ };
342
+ },
343
+ });
344
+
345
+ // --- Optional Public Tool ---
346
+
347
+ const registerTool = defineTool({
348
+ name: "register",
349
+ description: "Register a new agent client (self-service)",
350
+ visibility: "public",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ name: { type: "string", description: "Name for this client" },
355
+ scopes: {
356
+ type: "array",
357
+ items: { type: "string" },
358
+ description: "Requested scopes",
359
+ },
360
+ },
361
+ required: ["name"],
362
+ },
363
+ execute: async (input: { name: string; scopes?: string[] }) => {
364
+ let scopes = input.scopes ?? [];
365
+
366
+ // If registration scopes are restricted, filter
367
+ if (registrationScopes && registrationScopes.length > 0) {
368
+ scopes = scopes.filter((s) => registrationScopes.includes(s));
369
+ }
370
+
371
+ const { clientId, clientSecret } = await store.createClient(
372
+ input.name,
373
+ scopes,
374
+ true,
375
+ );
376
+
377
+ return { clientId, clientSecret, scopes };
378
+ },
379
+ });
380
+
381
+ // --- Private Tools (root key only) ---
382
+
383
+ const createClientTool = defineTool({
384
+ name: "create_client",
385
+ description: "Create a new client with specific scopes (admin only)",
386
+ visibility: "private",
387
+ inputSchema: {
388
+ type: "object",
389
+ properties: {
390
+ name: { type: "string", description: "Client name" },
391
+ scopes: {
392
+ type: "array",
393
+ items: { type: "string" },
394
+ description: "Scopes to grant",
395
+ },
396
+ },
397
+ required: ["name", "scopes"],
398
+ },
399
+ execute: async (input: { name: string; scopes: string[] }) => {
400
+ const { clientId, clientSecret } = await store.createClient(
401
+ input.name,
402
+ input.scopes,
403
+ );
404
+ return { clientId, clientSecret, scopes: input.scopes };
405
+ },
406
+ });
407
+
408
+ const listClientsTool = defineTool({
409
+ name: "list_clients",
410
+ description: "List all registered clients (admin only)",
411
+ visibility: "private",
412
+ inputSchema: { type: "object", properties: {} },
413
+ execute: async () => {
414
+ const clients = await store.listClients();
415
+ return {
416
+ clients: clients.map((c) => ({
417
+ clientId: c.clientId,
418
+ name: c.name,
419
+ scopes: c.scopes,
420
+ createdAt: c.createdAt,
421
+ selfRegistered: c.selfRegistered ?? false,
422
+ })),
423
+ };
424
+ },
425
+ });
426
+
427
+ const revokeClientTool = defineTool({
428
+ name: "revoke_client",
429
+ description: "Revoke a client and all its tokens (admin only)",
430
+ visibility: "private",
431
+ inputSchema: {
432
+ type: "object",
433
+ properties: {
434
+ clientId: { type: "string", description: "Client ID to revoke" },
435
+ },
436
+ required: ["clientId"],
437
+ },
438
+ execute: async (input: { clientId: string }) => {
439
+ const revoked = await store.revokeClient(input.clientId);
440
+ return { revoked };
441
+ },
442
+ });
443
+
444
+ const rotateSecretTool = defineTool({
445
+ name: "rotate_secret",
446
+ description: "Rotate a client's secret (admin only)",
447
+ visibility: "private",
448
+ inputSchema: {
449
+ type: "object",
450
+ properties: {
451
+ clientId: { type: "string", description: "Client ID to rotate" },
452
+ },
453
+ required: ["clientId"],
454
+ },
455
+ execute: async (input: { clientId: string }) => {
456
+ const result = await store.rotateSecret(input.clientId);
457
+ if (!result) throw new Error(`Client not found: ${input.clientId}`);
458
+ return { clientId: input.clientId, clientSecret: result.clientSecret };
459
+ },
460
+ });
461
+
462
+ // --- Assemble tools ---
463
+
464
+ const tools = [
465
+ tokenTool,
466
+ whoamiTool,
467
+ ...(allowRegistration ? [registerTool] : []),
468
+ createClientTool,
469
+ listClientsTool,
470
+ revokeClientTool,
471
+ rotateSecretTool,
472
+ ];
473
+
474
+ const agent = defineAgent({
475
+ path: "@auth",
476
+ entrypoint:
477
+ "Authentication agent. Provides OAuth2 client_credentials authentication for the agent network.",
478
+ config: {
479
+ name: "Auth",
480
+ description: "Built-in authentication agent",
481
+ supportedActions: ["execute_tool", "describe_tools", "load"],
482
+ },
483
+ tools: tools as ToolDefinition<ToolContext>[],
484
+ visibility: "public",
485
+ });
486
+
487
+ // Attach store and config for server integration
488
+ return Object.assign(agent, {
489
+ __authStore: store,
490
+ __rootKey: rootKey,
491
+ __tokenTtl: tokenTtl,
492
+ });
493
+ }