@slashfi/agents-sdk 0.8.0 → 0.10.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.
Files changed (59) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -0
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +135 -1
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +19 -0
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +218 -5
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/index.d.ts +9 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/integration-interface.d.ts +37 -0
  14. package/dist/integration-interface.d.ts.map +1 -0
  15. package/dist/integration-interface.js +94 -0
  16. package/dist/integration-interface.js.map +1 -0
  17. package/dist/integrations-store.d.ts +33 -0
  18. package/dist/integrations-store.d.ts.map +1 -0
  19. package/dist/integrations-store.js +50 -0
  20. package/dist/integrations-store.js.map +1 -0
  21. package/dist/jwt.d.ts +86 -17
  22. package/dist/jwt.d.ts.map +1 -1
  23. package/dist/jwt.js +140 -17
  24. package/dist/jwt.js.map +1 -1
  25. package/dist/registry.d.ts +7 -0
  26. package/dist/registry.d.ts.map +1 -1
  27. package/dist/registry.js +8 -21
  28. package/dist/registry.js.map +1 -1
  29. package/dist/secret-collection.d.ts +37 -0
  30. package/dist/secret-collection.d.ts.map +1 -0
  31. package/dist/secret-collection.js +37 -0
  32. package/dist/secret-collection.js.map +1 -0
  33. package/dist/server.d.ts +41 -44
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +236 -592
  36. package/dist/server.js.map +1 -1
  37. package/dist/types.d.ts +7 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +5 -2
  40. package/src/agent-definitions/auth.ts +187 -1
  41. package/src/agent-definitions/integrations.ts +260 -5
  42. package/src/index.ts +18 -4
  43. package/src/integration-interface.ts +118 -0
  44. package/src/integrations-store.ts +84 -0
  45. package/src/jwt.ts +233 -65
  46. package/src/registry.ts +17 -2
  47. package/src/secret-collection.ts +66 -0
  48. package/src/server.ts +272 -681
  49. package/src/types.ts +8 -1
  50. package/dist/slack-oauth.d.ts +0 -27
  51. package/dist/slack-oauth.d.ts.map +0 -1
  52. package/dist/slack-oauth.js +0 -48
  53. package/dist/slack-oauth.js.map +0 -1
  54. package/dist/web-pages.d.ts +0 -8
  55. package/dist/web-pages.d.ts.map +0 -1
  56. package/dist/web-pages.js +0 -169
  57. package/dist/web-pages.js.map +0 -1
  58. package/src/slack-oauth.ts +0 -66
  59. package/src/web-pages.ts +0 -178
package/src/server.ts CHANGED
@@ -1,30 +1,28 @@
1
1
  /**
2
2
  * Agent Server (MCP over HTTP)
3
3
  *
4
- * JSON-RPC server implementing the MCP protocol for agent interaction.
5
- * Compatible with atlas-environments and any MCP client.
4
+ * Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
5
+ * Handles only core SDK concerns:
6
+ * - MCP protocol (initialize, tools/list, tools/call)
7
+ * - Agent registry routing (call_agent, list_agents)
8
+ * - Auth resolution (Bearer tokens, root key, JWT)
9
+ * - OAuth2 token exchange (client_credentials)
10
+ * - Health check
11
+ * - CORS
6
12
  *
7
- * MCP Methods:
8
- * - initialize → Protocol handshake
9
- * - tools/list → List available MCP tools (call_agent, list_agents)
10
- * - tools/call → Execute an MCP tool
13
+ * Application-specific routes (web UI, OAuth callbacks, tenant management)
14
+ * should be built on top using the exported `fetch` handler.
11
15
  *
12
- * MCP Tools exposed:
13
- * - call_agent → Execute a tool on a registered agent
14
- * - list_agents → List registered agents and their tools
16
+ * @example
17
+ * ```typescript
18
+ * // Standalone usage
19
+ * const server = createAgentServer(registry, { port: 3000 });
20
+ * await server.start();
15
21
  *
16
- * Additional endpoints:
17
- * - POST /oauth/token → OAuth2 client_credentials (when @auth registered)
18
- * - GET /oauth/callback → Unified OAuth callback (provider from state)
19
- * - GET /integrations/callback/* → Legacy OAuth callback (provider from URL path)
20
- * - GET /health → Health check
21
- *
22
- * Auth Integration:
23
- * When an `@auth` agent is registered, the server automatically:
24
- * - Validates Bearer tokens on requests
25
- * - Resolves tokens to identity + scopes
26
- * - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
27
- * - Recognizes the root key for admin access
22
+ * // Composable with any HTTP framework
23
+ * const server = createAgentServer(registry);
24
+ * app.all('/mcp/*', (req) => server.fetch(req));
25
+ * ```
28
26
  */
29
27
 
30
28
  import type { AuthStore } from "./agent-definitions/auth.js";
@@ -33,17 +31,10 @@ import {
33
31
  processSecretParams,
34
32
  } from "./agent-definitions/secrets.js";
35
33
  import { verifyJwt } from "./jwt.js";
34
+ import type { SigningKey } from "./jwt.js";
35
+ import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
36
36
  import type { AgentRegistry } from "./registry.js";
37
37
  import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
38
- import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
39
- import { slackAuthUrl, exchangeSlackCode, getSlackProfile, type SlackOAuthConfig } from "./slack-oauth.js";
40
-
41
-
42
- function resolveBaseUrl(req: Request, url: URL): string {
43
- const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
44
- const host = req.headers.get("x-forwarded-host") || url.host;
45
- return `${proto}://${host}`;
46
- }
47
38
 
48
39
  // ============================================
49
40
  // Server Types
@@ -62,9 +53,12 @@ export interface AgentServerOptions {
62
53
  serverName?: string;
63
54
  /** Server version reported in MCP initialize (default: '1.0.0') */
64
55
  serverVersion?: string;
65
-
66
56
  /** Secret store for handling secret: refs in tool params */
67
57
  secretStore?: SecretStore;
58
+ /** URLs of trusted registries for cross-registry JWT verification */
59
+ trustedIssuers?: string[];
60
+ /** Pre-generated signing key (if not provided, one is generated on start) */
61
+ signingKey?: SigningKey;
68
62
  }
69
63
 
70
64
  export interface AgentServer {
@@ -72,10 +66,12 @@ export interface AgentServer {
72
66
  start(): Promise<void>;
73
67
  /** Stop the server */
74
68
  stop(): Promise<void>;
75
- /** Handle a request (for custom integrations) */
69
+ /** Handle a request (for custom integrations / framework composition) */
76
70
  fetch(req: Request): Promise<Response>;
77
71
  /** Get the server URL (only available after start) */
78
72
  url: string | null;
73
+ /** The agent registry this server uses */
74
+ registry: AgentRegistry;
79
75
  }
80
76
 
81
77
  // ============================================
@@ -97,79 +93,24 @@ interface JsonRpcResponse {
97
93
  }
98
94
 
99
95
  // ============================================
100
- // Auth Types
96
+ // Auth Types (exported for use by custom routes)
101
97
  // ============================================
102
98
 
103
- interface AuthConfig {
99
+ export interface AuthConfig {
104
100
  store: AuthStore;
105
101
  rootKey: string;
106
102
  tokenTtl: number;
107
103
  }
108
104
 
109
- interface ResolvedAuth {
105
+ export interface ResolvedAuth {
110
106
  callerId: string;
111
107
  callerType: "agent" | "user" | "system";
112
108
  scopes: string[];
113
109
  isRoot: boolean;
114
110
  }
115
111
 
116
-
117
- // ============================================
118
- // Secrets Collection (one-time tokens)
119
- // ============================================
120
-
121
-
122
- function escHtml(s: string): string {
123
- return s.replace(/&/g,"\&amp;").replace(/</g,"\&lt;").replace(/>/g,"\&gt;").replace(/"/g,"\&quot;");
124
- }
125
-
126
- function renderSecretForm(token: string, pending: PendingCollection, baseUrl: string): string {
127
- const fields = pending.fields.map(f => `
128
- <div class="field">
129
- <label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
130
- ${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
131
- <input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
132
- </div>`).join("");
133
-
134
- return `<!DOCTYPE html>
135
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
136
- <style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;max-width:480px;width:100%}.header{display:flex;align-items:center;gap:12px;margin-bottom:8px}.lock{font-size:24px}h1{font-size:20px;font-weight:600}.subtitle{color:#8b949e;font-size:14px;margin-bottom:24px}.shield{display:inline-flex;align-items:center;gap:4px;background:#1a2332;border:1px solid #1f6feb33;color:#58a6ff;font-size:12px;padding:2px 8px;border-radius:12px;margin-bottom:20px}label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}.desc{font-size:12px;color:#8b949e;margin-bottom:4px}.badge{background:#3d1f00;color:#f0883e;font-size:10px;padding:1px 6px;border-radius:4px}.req{color:#f85149}input{width:100%;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:14px;margin-bottom:16px;outline:none}input:focus{border-color:#58a6ff;box-shadow:0 0 0 3px #1f6feb33}button{width:100%;padding:12px;background:#238636;border:none;border-radius:6px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}button:hover{background:#2ea043}button:disabled{opacity:.5;cursor:not-allowed}.footer{text-align:center;margin-top:16px;font-size:12px;color:#484f58}.error{background:#3d1418;border:1px solid #f8514966;color:#f85149;padding:10px 12px;border-radius:6px;font-size:13px;margin-bottom:16px;display:none}.ok{text-align:center;padding:40px 0}.ok .icon{font-size:48px;margin-bottom:12px}.ok h2{font-size:18px;margin-bottom:8px;color:#3fb950}.ok p{color:#8b949e;font-size:14px}.field{position:relative}</style></head><body>
137
- <div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
138
- <p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
139
- <div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
140
- <form id="f">${fields}<button type="submit">Submit Securely</button></form>
141
- <p class="footer">Expires in 10 minutes</p></div>
142
- <div class="card ok" id="ok" style="display:none"><div class="icon">✅</div><h2>Done</h2><p>Credentials stored securely. You can close this window.</p></div>
143
- <script>document.getElementById("f").addEventListener("submit",async e=>{e.preventDefault();const b=e.target.querySelector("button");b.disabled=true;b.textContent="Submitting...";try{const fd=new FormData(e.target),vals=Object.fromEntries(fd.entries());const r=await fetch("${baseUrl}/secrets/collect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:"${token}",values:vals})});const d=await r.json();if(d.success){document.getElementById("fc").style.display="none";document.getElementById("ok").style.display="block";}else throw new Error(d.error?.message||JSON.stringify(d));}catch(err){const el=document.getElementById("err");el.textContent=err.message;el.style.display="block";b.disabled=false;b.textContent="Submit Securely";}});</script></body></html>`;
144
- }
145
-
146
- export interface PendingCollection {
147
- /** Partial params already provided by agent */
148
- params: Record<string, unknown>;
149
- /** Target agent + tool to call after collection */
150
- agent: string;
151
- tool: string;
152
- /** Auth context from original request */
153
- auth: ResolvedAuth | null;
154
- /** Fields the form needs to collect */
155
- fields: Array<{ name: string; description?: string; secret: boolean; required: boolean }>;
156
- /** Created timestamp for expiry */
157
- createdAt: number;
158
- }
159
-
160
- export const pendingCollections = new Map<string, PendingCollection>();
161
-
162
- export function generateCollectionToken(): string {
163
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
164
- let token = "sc_";
165
- for (let i = 0; i < 32; i++) {
166
- token += chars[Math.floor(Math.random() * chars.length)];
167
- }
168
- return token;
169
- }
170
-
171
112
  // ============================================
172
- // Helpers
113
+ // HTTP Helpers
173
114
  // ============================================
174
115
 
175
116
  function jsonResponse(data: unknown, status = 200): Response {
@@ -184,10 +125,22 @@ function corsHeaders(): Record<string, string> {
184
125
  "Access-Control-Allow-Origin": "*",
185
126
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
186
127
  "Access-Control-Allow-Headers":
187
- "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Agent-Id, X-Atlas-Session-Id",
128
+ "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
188
129
  };
189
130
  }
190
131
 
132
+ function addCors(res: Response): Response {
133
+ const headers = new Headers(res.headers);
134
+ for (const [k, v] of Object.entries(corsHeaders())) {
135
+ headers.set(k, v);
136
+ }
137
+ return new Response(res.body, { status: res.status, headers });
138
+ }
139
+
140
+ // ============================================
141
+ // JSON-RPC Helpers
142
+ // ============================================
143
+
191
144
  function jsonRpcSuccess(id: unknown, result: unknown): JsonRpcResponse {
192
145
  return { jsonrpc: "2.0", id, result };
193
146
  }
@@ -220,10 +173,10 @@ function mcpResult(value: unknown, isError = false) {
220
173
  }
221
174
 
222
175
  // ============================================
223
- // Auth Detection
176
+ // Auth Detection & Resolution
224
177
  // ============================================
225
178
 
226
- function detectAuth(registry: AgentRegistry): AuthConfig | null {
179
+ export function detectAuth(registry: AgentRegistry): AuthConfig | null {
227
180
  const authAgent = registry.get("@auth") as
228
181
  | (AgentDefinition & {
229
182
  __authStore?: AuthStore;
@@ -241,9 +194,10 @@ function detectAuth(registry: AgentRegistry): AuthConfig | null {
241
194
  };
242
195
  }
243
196
 
244
- async function resolveAuth(
197
+ export async function resolveAuth(
245
198
  req: Request,
246
199
  authConfig: AuthConfig,
200
+ jwksOptions?: { signingKeys?: SigningKey[]; trustedIssuers?: string[] },
247
201
  ): Promise<ResolvedAuth | null> {
248
202
  const authHeader = req.headers.get("Authorization");
249
203
  if (!authHeader) return null;
@@ -251,6 +205,7 @@ async function resolveAuth(
251
205
  const [scheme, credential] = authHeader.split(" ", 2);
252
206
  if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
253
207
 
208
+ // Root key check
254
209
  if (credential === authConfig.rootKey) {
255
210
  return {
256
211
  callerId: "root",
@@ -260,12 +215,47 @@ async function resolveAuth(
260
215
  };
261
216
  }
262
217
 
263
- // Try JWT verification first (stateless)
264
- // JWT is signed with the client's secret hash
265
- // Decode payload to get client_id, look up client, verify signature
218
+ // Try ES256 verification against own signing keys
266
219
  const parts = credential.split(".");
220
+ if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
221
+ for (const key of jwksOptions.signingKeys) {
222
+ try {
223
+ const verified = await verifyJwtLocal(credential, key.publicKey);
224
+ if (verified) {
225
+ return {
226
+ callerId: verified.sub ?? verified.name ?? "unknown",
227
+ callerType: "agent",
228
+ scopes: verified.scopes ?? ["*"],
229
+ isRoot: false,
230
+ };
231
+ }
232
+ } catch {
233
+ continue;
234
+ }
235
+ }
236
+ }
237
+
238
+ // Try trusted issuers (remote JWKS verification)
239
+ if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
240
+ for (const issuer of jwksOptions.trustedIssuers) {
241
+ try {
242
+ const verified = await verifyJwtFromIssuer(credential, issuer);
243
+ if (verified) {
244
+ return {
245
+ callerId: verified.sub ?? verified.name ?? "unknown",
246
+ callerType: "agent",
247
+ scopes: verified.scopes ?? ["*"],
248
+ isRoot: false,
249
+ };
250
+ }
251
+ } catch {
252
+ continue;
253
+ }
254
+ }
255
+ }
256
+
257
+ // Try HMAC JWT verification (legacy, stateless)
267
258
  if (parts.length === 3) {
268
- // Looks like a JWT - decode payload to get client_id
269
259
  try {
270
260
  const payloadB64 = parts[1];
271
261
  const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
@@ -277,7 +267,6 @@ async function resolveAuth(
277
267
  };
278
268
 
279
269
  if (payload.sub) {
280
- // Look up client to get the signing secret (secret hash)
281
270
  const client = await authConfig.store.getClient(payload.sub);
282
271
  if (client) {
283
272
  const verified = await verifyJwt(credential, client.clientSecretHash);
@@ -309,7 +298,7 @@ async function resolveAuth(
309
298
  };
310
299
  }
311
300
 
312
- function canSeeAgent(
301
+ export function canSeeAgent(
313
302
  agent: AgentDefinition,
314
303
  auth: ResolvedAuth | null,
315
304
  ): boolean {
@@ -346,11 +335,11 @@ function getToolDefinitions() {
346
335
  },
347
336
  path: {
348
337
  type: "string",
349
- description: "Agent path (e.g. '@registry')",
338
+ description: "Agent path (e.g., '@my-agent')",
350
339
  },
351
340
  tool: {
352
341
  type: "string",
353
- description: "Tool name to call (for execute_tool)",
342
+ description: "Tool name to call",
354
343
  },
355
344
  params: {
356
345
  type: "object",
@@ -366,7 +355,8 @@ function getToolDefinitions() {
366
355
  },
367
356
  {
368
357
  name: "list_agents",
369
- description: "List all registered agents and their available tools.",
358
+ description:
359
+ "List all registered agents and their available tools.",
370
360
  inputSchema: {
371
361
  type: "object",
372
362
  properties: {},
@@ -393,13 +383,15 @@ export function createAgentServer(
393
383
  secretStore,
394
384
  } = options;
395
385
 
396
- let serverInstance: ReturnType<typeof Bun.serve> | null = null;
397
- let serverUrl: string | null = null;
386
+ // Signing keys for JWKS-based auth
387
+ const serverSigningKeys: SigningKey[] = [];
388
+ const configTrustedIssuers: string[] = options.trustedIssuers ?? [];
398
389
 
399
390
  const authConfig = detectAuth(registry);
391
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
400
392
 
401
393
  // ──────────────────────────────────────────
402
- // MCP JSON-RPC handler
394
+ // JSON-RPC handler
403
395
  // ──────────────────────────────────────────
404
396
 
405
397
  async function handleJsonRpc(
@@ -407,24 +399,21 @@ export function createAgentServer(
407
399
  auth: ResolvedAuth | null,
408
400
  ): Promise<JsonRpcResponse> {
409
401
  switch (request.method) {
410
- // MCP protocol handshake
411
402
  case "initialize":
412
403
  return jsonRpcSuccess(request.id, {
413
404
  protocolVersion: "2024-11-05",
414
- capabilities: { tools: {} },
405
+ capabilities: { tools: { listChanged: false } },
415
406
  serverInfo: { name: serverName, version: serverVersion },
416
407
  });
417
408
 
418
409
  case "notifications/initialized":
419
410
  return jsonRpcSuccess(request.id, {});
420
411
 
421
- // List MCP tools
422
412
  case "tools/list":
423
413
  return jsonRpcSuccess(request.id, {
424
414
  tools: getToolDefinitions(),
425
415
  });
426
416
 
427
- // Call an MCP tool
428
417
  case "tools/call": {
429
418
  const { name, arguments: args } = (request.params ?? {}) as {
430
419
  name: string;
@@ -435,7 +424,7 @@ export function createAgentServer(
435
424
  const result = await handleToolCall(name, args ?? {}, auth);
436
425
  return jsonRpcSuccess(request.id, result);
437
426
  } catch (err) {
438
- console.error("[server] Request error:", err);
427
+ console.error("[server] Tool call error:", err);
439
428
  return jsonRpcSuccess(
440
429
  request.id,
441
430
  mcpResult(
@@ -469,6 +458,10 @@ export function createAgentServer(
469
458
  const req = (args.request ?? args) as CallAgentRequest;
470
459
 
471
460
  // Inject auth context
461
+ // No auth = internal/trusted call (e.g., atlas-api → atlas-os RPC)
462
+ if (!auth) {
463
+ req.callerType = "system";
464
+ }
472
465
  if (auth) {
473
466
  req.callerId = auth.callerId;
474
467
  req.callerType = auth.callerType;
@@ -481,10 +474,8 @@ export function createAgentServer(
481
474
  }
482
475
 
483
476
  // Process secret params: resolve refs, store raw secrets
484
- // Auto-resolve secret:xxx refs in tool params before execution
485
477
  if ((req as any).params && secretStore) {
486
478
  const ownerId = auth?.callerId ?? "anonymous";
487
- // Find the tool schema to check for secret: true fields
488
479
  const agent = registry.get(req.path);
489
480
  const tool = agent?.tools.find((t) => t.name === (req as any).tool);
490
481
  const schema = tool?.inputSchema as any;
@@ -518,6 +509,7 @@ export function createAgentServer(
518
509
  const tv = t.visibility ?? "internal";
519
510
  if (auth?.isRoot) return true;
520
511
  if (tv === "public") return true;
512
+ if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous") return true;
521
513
  if (tv === "internal" && auth) return true;
522
514
  return false;
523
515
  })
@@ -532,7 +524,7 @@ export function createAgentServer(
532
524
  }
533
525
 
534
526
  // ──────────────────────────────────────────
535
- // OAuth2 token handler (unchanged)
527
+ // OAuth2 token handler
536
528
  // ──────────────────────────────────────────
537
529
 
538
530
  async function handleOAuthToken(req: Request): Promise<Response> {
@@ -578,583 +570,175 @@ export function createAgentServer(
578
570
  );
579
571
  }
580
572
 
581
- const client = await authConfig.store.validateClient(
582
- clientId,
583
- clientSecret,
584
- );
585
- if (!client) {
586
- return jsonResponse(
587
- {
588
- error: "invalid_client",
589
- error_description: "Invalid client credentials",
590
- },
591
- 401,
592
- );
593
- }
594
-
595
- // Delegate to @auth agent's token tool which generates proper JWTs
596
- const tokenResult = await registry.call({
597
- action: "execute_tool",
598
- path: "@auth",
599
- tool: "token",
600
- params: {
601
- grantType: "client_credentials",
602
- clientId,
603
- clientSecret,
604
- },
605
- context: {
606
- tenantId: "default",
607
- agentPath: "@auth",
608
- callerId: "oauth_endpoint",
573
+ try {
574
+ const result = await registry.call({
575
+ action: "execute_tool",
576
+ path: "@auth",
577
+ tool: "token",
578
+ params: { clientId, clientSecret },
609
579
  callerType: "system",
610
- },
611
- } as any);
580
+ });
581
+
582
+ const tokenResult = (result as any)?.result;
583
+ if (!tokenResult?.accessToken) {
584
+ return jsonResponse(
585
+ { error: "invalid_client", error_description: "Authentication failed" },
586
+ 401,
587
+ );
588
+ }
612
589
 
613
- // Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
614
- const callResponse = tokenResult as any;
615
- if (!callResponse.success) {
616
- return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
590
+ return jsonResponse({
591
+ access_token: tokenResult.accessToken,
592
+ token_type: "Bearer",
593
+ expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
594
+ refresh_token: tokenResult.refreshToken,
595
+ });
596
+ } catch (err) {
597
+ console.error("[oauth] Token error:", err);
598
+ return jsonResponse(
599
+ { error: "server_error", error_description: "Token exchange failed" },
600
+ 500,
601
+ );
617
602
  }
618
- const tokenData = callResponse.result;
619
-
620
- // accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
621
- const accessToken = tokenData.accessToken?.$agent_type === "secret"
622
- ? tokenData.accessToken.value
623
- : tokenData.accessToken;
624
-
625
- return jsonResponse({
626
- access_token: accessToken,
627
- token_type: tokenData.tokenType ?? "Bearer",
628
- expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
629
- scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
630
- });
631
603
  }
632
604
 
633
605
  // ──────────────────────────────────────────
634
- // HTTP request handler
635
- // ─��────────────────────────────────────────
606
+ // Main fetch handler
607
+ // ──────────────────────────────────────────
636
608
 
637
609
  async function fetch(req: Request): Promise<Response> {
638
- const url = new URL(req.url);
639
- const path = url.pathname.replace(basePath, "") || "/";
640
-
641
- // CORS preflight
642
- if (cors && req.method === "OPTIONS") {
643
- return new Response(null, { status: 204, headers: corsHeaders() });
644
- }
645
-
646
- const addCors = (response: Response): Response => {
647
- if (!cors) return response;
648
- const headers = new Headers(response.headers);
649
- for (const [key, value] of Object.entries(corsHeaders())) {
650
- headers.set(key, value);
651
- }
652
- return new Response(response.body, {
653
- status: response.status,
654
- statusText: response.statusText,
655
- headers,
656
- });
657
- };
658
-
659
- const auth = authConfig ? await resolveAuth(req, authConfig) : null;
660
-
661
610
  try {
662
- // MCP endpoint: POST / or POST /mcp
663
- if ((path === "/" || path === "/mcp") && req.method === "POST") {
664
- const body = (await req.json()) as JsonRpcRequest;
665
- const response = await handleJsonRpc(body, auth);
666
- return addCors(jsonResponse(response));
667
- }
668
-
669
- // OAuth2 token endpoint
670
- if (path === "/oauth/token" && req.method === "POST") {
671
- return addCors(await handleOAuthToken(req));
672
- }
611
+ const url = new URL(req.url);
612
+ const path = url.pathname.replace(basePath, "") || "/";
673
613
 
674
- // Health check
675
- if (path === "/health" && req.method === "GET") {
676
- return addCors(jsonResponse({ status: "ok" }));
677
- }
678
-
679
- // Backwards compat: GET /list (returns agents directly)
680
- if (path === "/list" && req.method === "GET") {
681
- const agents = registry.list();
682
- const visible = agents.filter((agent) => canSeeAgent(agent, auth));
683
- return addCors(
684
- jsonResponse({
685
- success: true,
686
- agents: visible.map((agent) => ({
687
- path: agent.path,
688
- name: agent.config?.name,
689
- description: agent.config?.description,
690
- supportedActions: agent.config?.supportedActions,
691
- integration: agent.config?.integration || null,
692
- tools: agent.tools
693
- .filter((t) => {
694
- const tv = t.visibility ?? "internal";
695
- if (auth?.isRoot) return true;
696
- if (tv === "public") return true;
697
- if (tv === "internal" && auth) return true;
698
- return false;
699
- })
700
- .map((t) => t.name),
701
- })),
702
- }),
703
- );
614
+ // CORS preflight
615
+ if (cors && req.method === "OPTIONS") {
616
+ return new Response(null, { status: 204, headers: corsHeaders() });
704
617
  }
705
618
 
706
-
707
- // ---- Shared OAuth callback handler ----
708
- async function handleIntegrationOAuthCallback(provider: string, req: Request): Promise<Response> {
709
- const url = new URL(req.url);
710
- const code = url.searchParams.get("code");
711
- const state = url.searchParams.get("state");
712
- const oauthError = url.searchParams.get("error");
713
- const errorDescription = url.searchParams.get("error_description");
714
-
715
- if (oauthError) {
716
- return new Response(
717
- `<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`,
718
- { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } },
719
- );
720
- }
721
-
722
- if (!code) {
723
- return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
724
- }
725
-
726
- try {
727
- await registry.call({
728
- action: "execute_tool",
729
- path: "@integrations",
730
- tool: "handle_oauth_callback",
731
- params: { provider, code, state: state ?? undefined },
732
- context: {
733
- tenantId: "default",
734
- agentPath: "@integrations",
735
- callerId: "oauth_callback",
736
- callerType: "system",
737
- },
738
- } as any);
739
-
740
- // Parse redirect URL from state (base64-encoded JSON)
741
- let redirectUrl = "/";
742
- if (state) {
743
- try {
744
- const parsed = JSON.parse(atob(state));
745
- if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
746
- } catch {
747
- // Fallback: try raw JSON for backward compat
748
- try {
749
- const parsed = JSON.parse(state);
750
- if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
751
- } catch {}
619
+ // Resolve auth for all requests
620
+ const auth = authConfig ? await resolveAuth(req, authConfig, {
621
+ signingKeys: serverSigningKeys,
622
+ trustedIssuers: configTrustedIssuers,
623
+ }) : null;
624
+
625
+ // Also check header-based identity (for proxied requests)
626
+ const headerAuth: ResolvedAuth | null = !auth
627
+ ? (() => {
628
+ const actorId = req.headers.get("X-Atlas-Actor-Id");
629
+ const actorType = req.headers.get("X-Atlas-Actor-Type");
630
+ if (actorId) {
631
+ return {
632
+ callerId: actorId,
633
+ callerType: (actorType as any) ?? "agent",
634
+ scopes: ["*"],
635
+ isRoot: false,
636
+ };
752
637
  }
753
- }
638
+ return null;
639
+ })()
640
+ : null;
754
641
 
755
- const sep = redirectUrl.includes("?") ? "&" : "?";
756
- return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
757
- } catch (err) {
758
- return new Response(
759
- `<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`,
760
- { status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } },
761
- );
762
- }
763
- }
642
+ const effectiveAuth = auth ?? headerAuth;
764
643
 
765
- // GET /oauth/callback - Unified OAuth callback (provider from state param)
766
- if (path === "/oauth/callback" && req.method === "GET") {
767
- const url = new URL(req.url);
768
- const state = url.searchParams.get("state");
769
- let provider: string | undefined;
770
- if (state) {
771
- try {
772
- const parsed = JSON.parse(atob(state));
773
- provider = parsed.providerId;
774
- } catch {
775
- // Fallback: try raw JSON for backward compat
776
- try {
777
- const parsed = JSON.parse(state);
778
- provider = parsed.providerId;
779
- } catch {}
780
- }
781
- }
782
- if (!provider) {
783
- return addCors(jsonResponse({ error: "Missing provider in state param" }, 400));
784
- }
785
- return handleIntegrationOAuthCallback(provider, req);
786
- }
787
-
788
- // GET /integrations/callback/:provider - Legacy OAuth callback (provider from URL path)
789
- if (path.startsWith("/integrations/callback/") && req.method === "GET") {
790
- const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
791
- if (!provider) {
792
- return addCors(jsonResponse({ error: "Missing provider" }, 400));
793
- }
794
- return handleIntegrationOAuthCallback(provider, req);
795
- }
796
-
797
-
798
- // GET /secrets/form/:token - Serve hosted secrets form
799
- if (path.startsWith("/secrets/form/") && req.method === "GET") {
800
- const token = path.split("/").pop() ?? "";
801
- const pending = pendingCollections.get(token);
802
- if (!pending) {
803
- return addCors(new Response("Invalid or expired form link", { status: 404 }));
804
- }
805
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
806
- pendingCollections.delete(token);
807
- return addCors(new Response("Form link expired", { status: 410 }));
808
- }
809
- const reqUrl = new URL(req.url); const baseUrl = resolveBaseUrl(req, reqUrl);
810
- const html = renderSecretForm(token, pending, baseUrl);
811
- return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
812
- }
813
-
814
- // POST /secrets/collect - Submit collected secrets and auto-forward to tool
815
- if (path === "/secrets/collect" && req.method === "POST") {
816
- const body = (await req.json()) as {
817
- token: string;
818
- values: Record<string, string>;
819
- };
820
-
821
- const pending = pendingCollections.get(body.token);
822
- if (!pending) {
823
- return addCors(
824
- jsonResponse({ error: "Invalid or expired collection token" }, 400),
825
- );
826
- }
827
-
828
- // One-time use
829
- pendingCollections.delete(body.token);
830
-
831
- // Check expiry (10 min)
832
- if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
833
- return addCors(
834
- jsonResponse({ error: "Collection token expired" }, 400),
835
- );
836
- }
837
-
838
- // Encrypt secret values and store as refs
839
- const mergedParams = { ...pending.params };
840
- for (const [fieldName, value] of Object.entries(body.values)) {
841
- const fieldDef = pending.fields.find((f) => f.name === fieldName);
842
- if (fieldDef?.secret && secretStore) {
843
- // Store encrypted, get ref
844
- const ownerId = pending.auth?.callerId ?? "anonymous";
845
- const secretId = await secretStore.store(value, ownerId);
846
- mergedParams[fieldName] = `secret:${secretId}`;
847
- } else {
848
- mergedParams[fieldName] = value;
849
- }
850
- }
851
-
852
- // Auto-forward to the target tool
853
- const callRequest = {
854
- action: "execute_tool" as const,
855
- path: pending.agent,
856
- tool: pending.tool,
857
- params: mergedParams,
858
- };
859
-
860
- const toolCtx = {
861
- tenantId: "default",
862
- agentPath: pending.agent,
863
- callerId: pending.auth?.callerId ?? "anonymous",
864
- callerType: pending.auth?.callerType ?? ("system" as const),
865
- };
866
-
867
- const result = await registry.call({
868
- ...callRequest,
869
- context: toolCtx,
870
- } as any);
871
-
872
- return addCors(jsonResponse({ success: true, result }));
873
- }
874
-
875
-
876
- // --- Web pages (plain HTML, served from same server) ---
877
- const htmlRes = (body: string) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
878
- const reqUrl = new URL(req.url);
879
- const baseUrl = resolveBaseUrl(req, reqUrl);
880
-
881
- const slackConfig: SlackOAuthConfig | null =
882
- process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
883
- ? {
884
- clientId: process.env.SLACK_CLIENT_ID,
885
- clientSecret: process.env.SLACK_CLIENT_SECRET,
886
- redirectUri: `${baseUrl}/auth/slack/callback`,
887
- }
888
- : null;
889
-
890
- // Helper: read session from cookie
891
- function getSession(r: Request): Record<string, any> | null {
892
- const c = r.headers.get("Cookie") || "";
893
- const m = c.match(/s_session=([^;]+)/);
894
- if (!m) return null;
895
- try { return JSON.parse(Buffer.from(m[1], "base64url").toString()); }
896
- catch { return null; }
897
- }
898
-
899
- // Helper: generate JWT from client credentials
900
- async function generateMcpToken(): Promise<string> {
901
- const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
902
- name: "mcp-" + Date.now(),
903
- scopes: ["*"],
904
- }} as any) as any;
905
- const cid = clientRes?.result?.clientId;
906
- const csec = clientRes?.result?.clientSecret;
907
- if (!cid || !csec) throw new Error("Failed to create client: " + JSON.stringify(clientRes));
908
-
909
- const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
910
- method: "POST",
911
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
912
- body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
913
- });
914
- const tokenData = await tokenRes.json() as any;
915
- if (!tokenData.access_token) throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
916
- return tokenData.access_token;
917
- }
918
-
919
- // Helper: set session cookie and redirect
920
- function sessionRedirect(location: string, session: Record<string, any>): Response {
921
- const data = Buffer.from(JSON.stringify(session)).toString("base64url");
922
- return new Response(null, {
923
- status: 302,
924
- headers: {
925
- Location: location,
926
- "Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
927
- },
928
- });
929
- }
930
-
931
- // GET / — login page (or redirect to dashboard if session exists)
932
- if (path === "/" && req.method === "GET") {
933
- const session = getSession(req);
934
- if (session?.token) return Response.redirect(`${baseUrl}/dashboard`, 302);
935
- return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
936
- }
937
-
938
- // GET /auth/slack — start Slack OAuth
939
- if (path === "/auth/slack" && req.method === "GET") {
940
- if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
941
- return Response.redirect(slackAuthUrl(slackConfig), 302);
644
+ // ── POST / MCP JSON-RPC ──
645
+ if (path === "/" && req.method === "POST") {
646
+ const body = (await req.json()) as JsonRpcRequest;
647
+ const result = await handleJsonRpc(body, effectiveAuth);
648
+ return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
942
649
  }
943
650
 
944
- // GET /auth/slack/callback handle Slack OAuth callback
945
- if (path === "/auth/slack/callback" && req.method === "GET") {
946
- if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
947
- const authCode = reqUrl.searchParams.get("code");
948
- const authError = reqUrl.searchParams.get("error");
949
- if (authError || !authCode) return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
950
-
951
- try {
952
- const tokens = await exchangeSlackCode(authCode, slackConfig);
953
- const profile = await getSlackProfile(tokens.access_token);
954
- const teamId = profile["https://slack.com/team_id"] || "";
955
- const teamName = profile["https://slack.com/team_name"] || "";
956
-
957
- // Check if user already exists
958
- console.log("[auth] Looking up slack user:", profile.sub, profile.email);
959
- const existing = await registry.call({
960
- action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
961
- params: { provider: "slack", providerUserId: profile.sub },
962
- } as any) as any;
963
- console.log("[auth] resolve_identity:", JSON.stringify(existing));
964
-
965
- if (existing?.result?.found && existing?.result?.user?.tenantId) {
966
- // Returning user — generate token and go to dashboard
967
- console.log("[auth] Returning user, generating token...");
968
- const mcpToken = await generateMcpToken();
969
- return sessionRedirect(`${baseUrl}/dashboard`, {
970
- userId: existing.result.user.id,
971
- tenantId: existing.result.user.tenantId,
972
- email: existing.result.user.email,
973
- name: existing.result.user.name,
974
- token: mcpToken,
975
- });
976
- }
977
-
978
- // Check if Slack team already has a tenant
979
- if (teamId) {
980
- console.log("[auth] Checking tenant_identities for team:", teamId);
981
- try {
982
- // Direct DB query via the auth store's underlying connection
983
- // We'll use a simple fetch to our own MCP endpoint to call @db-connections
984
- // Actually, simpler: just query the DB directly via the server's context
985
- // For now, use registry.call to a custom tool or direct SQL
986
- // Simplest: call @auth list_tenants and check metadata
987
- // Even simpler: direct SQL via globalThis.fetch to ourselves
988
- const dbUrl = process.env.DATABASE_URL;
989
- if (dbUrl) {
990
- const { default: postgres } = await import("postgres");
991
- const sql = postgres(dbUrl);
992
- const rows = await sql`SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
993
- await sql.end();
994
- if (rows.length > 0) {
995
- const existingTenantId = rows[0].tenant_id;
996
- console.log("[auth] Found existing tenant for team:", existingTenantId);
997
-
998
- // Create user on existing tenant
999
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
1000
- email: profile.email, name: profile.name, tenantId: existingTenantId,
1001
- }} as any) as any;
1002
- const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
1003
- console.log("[auth] Created user on existing tenant:", newUserId);
1004
-
1005
- // Link identity
1006
- if (newUserId) {
1007
- await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
1008
- userId: newUserId, provider: "slack", providerUserId: profile.sub,
1009
- email: profile.email, name: profile.name,
1010
- metadata: { slackTeamId: teamId, slackTeamName: teamName },
1011
- }} as any);
1012
- }
1013
-
1014
- // Generate token and go to dashboard
1015
- const mcpToken = await generateMcpToken();
1016
- return sessionRedirect(`${baseUrl}/dashboard`, {
1017
- userId: newUserId, tenantId: existingTenantId,
1018
- email: profile.email, name: profile.name, token: mcpToken,
1019
- });
1020
- }
1021
- }
1022
- } catch (e: any) {
1023
- console.error("[auth] tenant_identity lookup error:", e.message);
1024
- }
1025
- }
1026
-
1027
- // New user — redirect to setup
1028
- return sessionRedirect(`${baseUrl}/setup`, {
1029
- email: profile.email,
1030
- name: profile.name,
1031
- picture: profile.picture,
1032
- slackUserId: profile.sub,
1033
- slackTeamId: teamId,
1034
- slackTeamName: teamName,
1035
- });
1036
- } catch (err: any) {
1037
- console.error("[auth] callback error:", err);
1038
- return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
1039
- }
651
+ // ── POST /oauth/token OAuth2 client_credentials ──
652
+ if (path === "/oauth/token" && req.method === "POST") {
653
+ const res = await handleOAuthToken(req);
654
+ return cors ? addCors(res) : res;
1040
655
  }
1041
656
 
1042
- // GET /setup tenant creation page
1043
- if (path === "/setup" && req.method === "GET") {
1044
- const session = getSession(req);
1045
- if (!session?.email) return Response.redirect(`${baseUrl}/`, 302);
1046
- return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
657
+ // ── GET /health Health check ──
658
+ if (path === "/health" && req.method === "GET") {
659
+ const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
660
+ return cors ? addCors(res) : res;
1047
661
  }
1048
662
 
1049
- // POST /setup create tenant + user + link identity + generate token
1050
- if (path === "/setup" && req.method === "POST") {
1051
- try {
1052
- const body = await req.json() as { email?: string; tenant?: string; name?: string };
1053
- const session = getSession(req);
1054
- console.log("[setup] body:", JSON.stringify(body), "session:", JSON.stringify(session));
1055
-
1056
- // 1. Create tenant
1057
- const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } } as any) as any;
1058
- const tenantId = tenantRes?.result?.tenantId;
1059
- if (!tenantId) return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
1060
- console.log("[setup] tenant created:", tenantId);
1061
-
1062
- // 2. Create user
1063
- const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } } as any) as any;
1064
- const userId = userRes?.result?.id || userRes?.result?.user?.id;
1065
- console.log("[setup] user created:", userId);
1066
-
1067
- // 2b. Link tenant to Slack team
1068
- if (session?.slackTeamId) {
1069
- try {
1070
- const dbUrl = process.env.DATABASE_URL;
1071
- if (dbUrl) {
1072
- const { default: postgres } = await import("postgres");
1073
- const sql = postgres(dbUrl);
1074
- const id = "ti_" + Math.random().toString(36).slice(2, 14);
1075
- await sql`INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
1076
- await sql.end();
1077
- console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
1078
- }
1079
- } catch (e: any) { console.error("[setup] tenant_identity insert error:", e.message); }
1080
- }
1081
-
1082
- // 3. Link Slack identity
1083
- if (session?.slackUserId && userId) {
1084
- console.log("[setup] linking slack identity:", session.slackUserId);
1085
- const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
1086
- userId,
1087
- provider: "slack",
1088
- providerUserId: session.slackUserId,
1089
- email: body.email,
1090
- name: session.name,
1091
- metadata:{ slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system" }} as any);
1092
- console.log("[setup] link_identity result:", JSON.stringify(linkRes));
1093
- }
1094
-
1095
- // 4. Generate MCP token
1096
- const mcpToken = await generateMcpToken();
1097
- console.log("[setup] token generated, length:", mcpToken.length);
1098
-
1099
- return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
1100
- } catch (err: any) {
1101
- console.error("[setup] error:", err);
1102
- return addCors(jsonResponse({ error: err.message }, 400));
1103
- }
663
+ // ── GET /.well-known/jwks.json JWKS public keys ──
664
+ if (path === "/.well-known/jwks.json" && req.method === "GET") {
665
+ const jwks = serverSigningKeys.length > 0
666
+ ? await buildJwks(serverSigningKeys)
667
+ : { keys: [] };
668
+ const res = jsonResponse(jwks);
669
+ return cors ? addCors(res) : res;
1104
670
  }
1105
671
 
1106
- // POST /logout clear session
1107
- if (path === "/logout" && req.method === "POST") {
1108
- return new Response(null, {
1109
- status: 302,
1110
- headers: {
1111
- Location: `${baseUrl}/`,
1112
- "Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
1113
- },
672
+ // ── GET /.well-known/configuration Server discovery ──
673
+ if (path === "/.well-known/configuration" && req.method === "GET") {
674
+ const baseUrl = new URL(req.url).origin;
675
+ const res = jsonResponse({
676
+ issuer: baseUrl,
677
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
678
+ token_endpoint: `${baseUrl}/oauth/token`,
679
+ agents_endpoint: `${baseUrl}/list`,
680
+ call_endpoint: baseUrl,
681
+ supported_grant_types: ["client_credentials"],
682
+ agents: registry.listPaths(),
1114
683
  });
684
+ return cors ? addCors(res) : res;
1115
685
  }
1116
686
 
1117
- // GET /dashboard show MCP URL and setup instructions
1118
- if (path === "/dashboard" && req.method === "GET") {
1119
- const session = getSession(req);
1120
- let token = session?.token || reqUrl.searchParams.get("token") || "";
1121
- if (!token) return Response.redirect(`${baseUrl}/`, 302);
1122
-
1123
- // Persist token in cookie
1124
- const sessData = Buffer.from(JSON.stringify({ ...session, token })).toString("base64url");
1125
- return new Response(renderDashboardPage(baseUrl, token, session || undefined), {
1126
- headers: {
1127
- "Content-Type": "text/html; charset=utf-8",
1128
- "Set-Cookie": `s_session=${sessData}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
1129
- },
1130
- });
687
+ // ── GET /list List agents (legacy endpoint) ──
688
+ if (path === "/list" && req.method === "GET") {
689
+ const agents = registry.list();
690
+ const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
691
+ const res = jsonResponse(
692
+ visible.map((agent) => ({
693
+ path: agent.path,
694
+ name: agent.config?.name,
695
+ description: agent.config?.description,
696
+ supportedActions: agent.config?.supportedActions,
697
+ integration: agent.config?.integration || null,
698
+ tools: agent.tools
699
+ .filter((t) => {
700
+ const tv = t.visibility ?? "internal";
701
+ if (effectiveAuth?.isRoot) return true;
702
+ if (tv === "public") return true;
703
+ if (tv === "internal" && effectiveAuth) return true;
704
+ return false;
705
+ })
706
+ .map((t) => ({
707
+ name: t.name,
708
+ description: t.description,
709
+ })),
710
+ })),
711
+ );
712
+ return cors ? addCors(res) : res;
1131
713
  }
1132
714
 
1133
- return addCors(
1134
- jsonResponse(
1135
- {
1136
- jsonrpc: "2.0",
1137
- id: null,
1138
- error: {
1139
- code: -32601,
1140
- message: `Not found: ${req.method} ${path}`,
1141
- },
715
+ // ── Not found ──
716
+ const res = jsonResponse(
717
+ {
718
+ jsonrpc: "2.0",
719
+ id: null,
720
+ error: {
721
+ code: -32601,
722
+ message: `Not found: ${req.method} ${path}`,
1142
723
  },
1143
- 404,
1144
- ),
724
+ },
725
+ 404,
1145
726
  );
727
+ return cors ? addCors(res) : res;
1146
728
  } catch (err) {
1147
729
  console.error("[server] Request error:", err);
1148
- return addCors(
1149
- jsonResponse(
1150
- {
1151
- jsonrpc: "2.0",
1152
- id: null,
1153
- error: { code: -32603, message: "Internal error" },
730
+ const res = jsonResponse(
731
+ {
732
+ jsonrpc: "2.0",
733
+ id: null,
734
+ error: {
735
+ code: -32603,
736
+ message: err instanceof Error ? err.message : "Internal error",
1154
737
  },
1155
- 500,
1156
- ),
738
+ },
739
+ 500,
1157
740
  );
741
+ return cors ? addCors(res) : res;
1158
742
  }
1159
743
  }
1160
744
 
@@ -1162,38 +746,45 @@ export function createAgentServer(
1162
746
  // Server lifecycle
1163
747
  // ──────────────────────────────────────────
1164
748
 
1165
- const server: AgentServer = {
1166
- async start(): Promise<void> {
1167
- if (serverInstance) throw new Error("Server is already running");
1168
-
1169
- serverInstance = Bun.serve({ port, hostname, fetch });
1170
- serverUrl = `http://${hostname}:${port}${basePath}`;
1171
-
1172
- console.log(`Agent server running at ${serverUrl}`);
1173
- console.log(" POST / - MCP JSON-RPC endpoint");
1174
- console.log(" POST /mcp - MCP JSON-RPC endpoint (alias)");
1175
- console.log(" GET /health - Health check");
1176
- if (authConfig) {
1177
- console.log(" POST /oauth/token - OAuth2 token endpoint");
1178
- console.log(" Auth: enabled");
749
+ return {
750
+ url: null,
751
+ registry,
752
+
753
+ async start() {
754
+ // Load or generate signing key for JWKS
755
+ if (options.signingKey) {
756
+ serverSigningKeys.push(options.signingKey);
757
+ } else if (authConfig?.store?.getSigningKeys) {
758
+ const stored = await authConfig.store.getSigningKeys() ?? [];
759
+ for (const exported of stored) {
760
+ serverSigningKeys.push(await importSigningKey(exported));
761
+ }
762
+ }
763
+ if (serverSigningKeys.length === 0) {
764
+ const key = await generateSigningKey();
765
+ serverSigningKeys.push(key);
766
+ if (authConfig?.store?.storeSigningKey) {
767
+ await authConfig.store.storeSigningKey(await exportSigningKey(key));
768
+ }
1179
769
  }
1180
- console.log(" MCP tools: call_agent, list_agents");
770
+
771
+ serverInstance = Bun.serve({
772
+ port,
773
+ hostname,
774
+ fetch,
775
+ });
776
+ (this as any).url = `http://${hostname}:${port}`;
777
+ console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
1181
778
  },
1182
779
 
1183
- async stop(): Promise<void> {
780
+ async stop() {
1184
781
  if (serverInstance) {
1185
782
  serverInstance.stop();
1186
783
  serverInstance = null;
1187
- serverUrl = null;
784
+ (this as any).url = null;
1188
785
  }
1189
786
  },
1190
787
 
1191
788
  fetch,
1192
-
1193
- get url(): string | null {
1194
- return serverUrl;
1195
- },
1196
789
  };
1197
-
1198
- return server;
1199
790
  }