@slashfi/agents-sdk 0.4.0 → 0.6.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 (65) hide show
  1. package/dist/{auth.d.ts → agent-definitions/auth.d.ts} +36 -2
  2. package/dist/agent-definitions/auth.d.ts.map +1 -0
  3. package/dist/{auth.js → agent-definitions/auth.js} +69 -8
  4. package/dist/agent-definitions/auth.js.map +1 -0
  5. package/dist/agent-definitions/integrations.d.ts +162 -0
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -0
  7. package/dist/agent-definitions/integrations.js +861 -0
  8. package/dist/agent-definitions/integrations.js.map +1 -0
  9. package/dist/agent-definitions/secrets.d.ts +51 -0
  10. package/dist/agent-definitions/secrets.d.ts.map +1 -0
  11. package/dist/agent-definitions/secrets.js +165 -0
  12. package/dist/agent-definitions/secrets.js.map +1 -0
  13. package/dist/agent-definitions/users.d.ts +80 -0
  14. package/dist/agent-definitions/users.d.ts.map +1 -0
  15. package/dist/agent-definitions/users.js +397 -0
  16. package/dist/agent-definitions/users.js.map +1 -0
  17. package/dist/crypto.d.ts +14 -0
  18. package/dist/crypto.d.ts.map +1 -0
  19. package/dist/crypto.js +40 -0
  20. package/dist/crypto.js.map +1 -0
  21. package/dist/define.d.ts +6 -1
  22. package/dist/define.d.ts.map +1 -1
  23. package/dist/define.js +1 -0
  24. package/dist/define.js.map +1 -1
  25. package/dist/index.d.ts +10 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +9 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/jwt.d.ts +2 -0
  30. package/dist/jwt.d.ts.map +1 -1
  31. package/dist/jwt.js.map +1 -1
  32. package/dist/server.d.ts +28 -1
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +478 -27
  35. package/dist/server.js.map +1 -1
  36. package/dist/slack-oauth.d.ts +27 -0
  37. package/dist/slack-oauth.d.ts.map +1 -0
  38. package/dist/slack-oauth.js +48 -0
  39. package/dist/slack-oauth.js.map +1 -0
  40. package/dist/types.d.ts +66 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/web-pages.d.ts +8 -0
  43. package/dist/web-pages.d.ts.map +1 -0
  44. package/dist/web-pages.js +169 -0
  45. package/dist/web-pages.js.map +1 -0
  46. package/package.json +2 -1
  47. package/src/{auth.ts → agent-definitions/auth.ts} +134 -15
  48. package/src/agent-definitions/integrations.ts +1209 -0
  49. package/src/agent-definitions/secrets.ts +241 -0
  50. package/src/agent-definitions/users.ts +533 -0
  51. package/src/crypto.ts +71 -0
  52. package/src/define.ts +8 -0
  53. package/src/index.ts +62 -4
  54. package/src/jwt.ts +9 -5
  55. package/src/server.ts +567 -35
  56. package/src/slack-oauth.ts +66 -0
  57. package/src/types.ts +83 -0
  58. package/src/web-pages.ts +178 -0
  59. package/dist/auth.d.ts.map +0 -1
  60. package/dist/auth.js.map +0 -1
  61. package/dist/secrets.d.ts +0 -44
  62. package/dist/secrets.d.ts.map +0 -1
  63. package/dist/secrets.js +0 -106
  64. package/dist/secrets.js.map +0 -1
  65. package/src/secrets.ts +0 -154
package/dist/server.d.ts CHANGED
@@ -24,8 +24,8 @@
24
24
  * - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
25
25
  * - Recognizes the root key for admin access
26
26
  */
27
+ import { type SecretStore } from "./agent-definitions/secrets.js";
27
28
  import type { AgentRegistry } from "./registry.js";
28
- import { type SecretStore } from "./secrets.js";
29
29
  export interface AgentServerOptions {
30
30
  /** Port to listen on (default: 3000) */
31
31
  port?: number;
@@ -52,5 +52,32 @@ export interface AgentServer {
52
52
  /** Get the server URL (only available after start) */
53
53
  url: string | null;
54
54
  }
55
+ interface ResolvedAuth {
56
+ callerId: string;
57
+ callerType: "agent" | "user" | "system";
58
+ scopes: string[];
59
+ isRoot: boolean;
60
+ }
61
+ export interface PendingCollection {
62
+ /** Partial params already provided by agent */
63
+ params: Record<string, unknown>;
64
+ /** Target agent + tool to call after collection */
65
+ agent: string;
66
+ tool: string;
67
+ /** Auth context from original request */
68
+ auth: ResolvedAuth | null;
69
+ /** Fields the form needs to collect */
70
+ fields: Array<{
71
+ name: string;
72
+ description?: string;
73
+ secret: boolean;
74
+ required: boolean;
75
+ }>;
76
+ /** Created timestamp for expiry */
77
+ createdAt: number;
78
+ }
79
+ export declare const pendingCollections: Map<string, PendingCollection>;
80
+ export declare function generateCollectionToken(): string;
55
81
  export declare function createAgentServer(registry: AgentRegistry, options?: AgentServerOptions): AgentServer;
82
+ export {};
56
83
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,EAAE,KAAK,WAAW,EAAkD,MAAM,cAAc,CAAC;AAMhG,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sBAAsB;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,iDAAiD;IACjD,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,sDAAsD;IACtD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAoOD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,EACvB,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CAiVb"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EACL,KAAK,WAAW,EAEjB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAgBnD,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sBAAsB;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,iDAAiD;IACjD,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,sDAAsD;IACtD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AA8BD,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACxC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAgCD,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC1B,uCAAuC;IACvC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC1F,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,kBAAkB,gCAAuC,CAAC;AAEvE,wBAAgB,uBAAuB,IAAI,MAAM,CAOhD;AAqND,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,EACvB,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CA8wBb"}
package/dist/server.js CHANGED
@@ -24,8 +24,48 @@
24
24
  * - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
25
25
  * - Recognizes the root key for admin access
26
26
  */
27
+ import { processSecretParams, } from "./agent-definitions/secrets.js";
27
28
  import { verifyJwt } from "./jwt.js";
28
- import { createInMemorySecretStore, processSecretParams } from "./secrets.js";
29
+ import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
30
+ import { slackAuthUrl, exchangeSlackCode, getSlackProfile } from "./slack-oauth.js";
31
+ function resolveBaseUrl(req, url) {
32
+ const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
33
+ const host = req.headers.get("x-forwarded-host") || url.host;
34
+ return `${proto}://${host}`;
35
+ }
36
+ // ============================================
37
+ // Secrets Collection (one-time tokens)
38
+ // ============================================
39
+ function escHtml(s) {
40
+ return s.replace(/&/g, "\&amp;").replace(/</g, "\&lt;").replace(/>/g, "\&gt;").replace(/"/g, "\&quot;");
41
+ }
42
+ function renderSecretForm(token, pending, baseUrl) {
43
+ const fields = pending.fields.map(f => `
44
+ <div class="field">
45
+ <label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
46
+ ${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
47
+ <input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
48
+ </div>`).join("");
49
+ return `<!DOCTYPE html>
50
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
51
+ <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>
52
+ <div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
53
+ <p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
54
+ <div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
55
+ <form id="f">${fields}<button type="submit">Submit Securely</button></form>
56
+ <p class="footer">Expires in 10 minutes</p></div>
57
+ <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>
58
+ <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>`;
59
+ }
60
+ export const pendingCollections = new Map();
61
+ export function generateCollectionToken() {
62
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
63
+ let token = "sc_";
64
+ for (let i = 0; i < 32; i++) {
65
+ token += chars[Math.floor(Math.random() * chars.length)];
66
+ }
67
+ return token;
68
+ }
29
69
  // ============================================
30
70
  // Helpers
31
71
  // ============================================
@@ -46,7 +86,11 @@ function jsonRpcSuccess(id, result) {
46
86
  return { jsonrpc: "2.0", id, result };
47
87
  }
48
88
  function jsonRpcError(id, code, message, data) {
49
- return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined && { data }) } };
89
+ return {
90
+ jsonrpc: "2.0",
91
+ id,
92
+ error: { code, message, ...(data !== undefined && { data }) },
93
+ };
50
94
  }
51
95
  /** Wrap a value as MCP tool result content */
52
96
  function mcpResult(value, isError = false) {
@@ -81,7 +125,12 @@ async function resolveAuth(req, authConfig) {
81
125
  if (scheme?.toLowerCase() !== "bearer" || !credential)
82
126
  return null;
83
127
  if (credential === authConfig.rootKey) {
84
- return { callerId: "root", callerType: "system", scopes: ["*"], isRoot: true };
128
+ return {
129
+ callerId: "root",
130
+ callerType: "system",
131
+ scopes: ["*"],
132
+ isRoot: true,
133
+ };
85
134
  }
86
135
  // Try JWT verification first (stateless)
87
136
  // JWT is signed with the client's secret hash
@@ -126,7 +175,9 @@ async function resolveAuth(req, authConfig) {
126
175
  };
127
176
  }
128
177
  function canSeeAgent(agent, auth) {
129
- const visibility = (agent.visibility ?? agent.config?.visibility ?? "internal");
178
+ const visibility = (agent.visibility ??
179
+ agent.config?.visibility ??
180
+ "internal");
130
181
  if (auth?.isRoot)
131
182
  return true;
132
183
  if (visibility === "public")
@@ -189,7 +240,7 @@ function getToolDefinitions() {
189
240
  // Create Server
190
241
  // ============================================
191
242
  export function createAgentServer(registry, options = {}) {
192
- const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore = createInMemorySecretStore(), } = options;
243
+ const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore, } = options;
193
244
  let serverInstance = null;
194
245
  let serverUrl = null;
195
246
  const authConfig = detectAuth(registry);
@@ -248,6 +299,7 @@ export function createAgentServer(registry, options = {}) {
248
299
  req.callerType = "system";
249
300
  }
250
301
  // Process secret params: resolve refs, store raw secrets
302
+ // Auto-resolve secret:xxx refs in tool params before execution
251
303
  if (req.params && secretStore) {
252
304
  const ownerId = auth?.callerId ?? "anonymous";
253
305
  // Find the tool schema to check for secret: true fields
@@ -270,6 +322,7 @@ export function createAgentServer(registry, options = {}) {
270
322
  name: agent.config?.name,
271
323
  description: agent.config?.description,
272
324
  supportedActions: agent.config?.supportedActions,
325
+ integration: agent.config?.integration || null,
273
326
  tools: agent.tools
274
327
  .filter((t) => {
275
328
  const tv = t.visibility ?? "internal";
@@ -314,29 +367,56 @@ export function createAgentServer(registry, options = {}) {
314
367
  clientSecret = body.client_secret ?? "";
315
368
  }
316
369
  if (grantType !== "client_credentials") {
317
- return jsonResponse({ error: "unsupported_grant_type", error_description: "Only client_credentials is supported" }, 400);
370
+ return jsonResponse({
371
+ error: "unsupported_grant_type",
372
+ error_description: "Only client_credentials is supported",
373
+ }, 400);
318
374
  }
319
375
  if (!clientId || !clientSecret) {
320
- return jsonResponse({ error: "invalid_request", error_description: "Missing client_id or client_secret" }, 400);
376
+ return jsonResponse({
377
+ error: "invalid_request",
378
+ error_description: "Missing client_id or client_secret",
379
+ }, 400);
321
380
  }
322
381
  const client = await authConfig.store.validateClient(clientId, clientSecret);
323
382
  if (!client) {
324
- return jsonResponse({ error: "invalid_client", error_description: "Invalid client credentials" }, 401);
383
+ return jsonResponse({
384
+ error: "invalid_client",
385
+ error_description: "Invalid client credentials",
386
+ }, 401);
325
387
  }
326
- const tokenString = `at_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
327
- const now = Date.now();
328
- await authConfig.store.storeToken({
329
- token: tokenString,
330
- clientId: client.clientId,
331
- scopes: client.scopes,
332
- issuedAt: now,
333
- expiresAt: now + authConfig.tokenTtl * 1000,
388
+ // Delegate to @auth agent's token tool which generates proper JWTs
389
+ const tokenResult = await registry.call({
390
+ action: "execute_tool",
391
+ path: "@auth",
392
+ tool: "token",
393
+ params: {
394
+ grantType: "client_credentials",
395
+ clientId,
396
+ clientSecret,
397
+ },
398
+ context: {
399
+ tenantId: "default",
400
+ agentPath: "@auth",
401
+ callerId: "oauth_endpoint",
402
+ callerType: "system",
403
+ },
334
404
  });
405
+ // Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
406
+ const callResponse = tokenResult;
407
+ if (!callResponse.success) {
408
+ return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
409
+ }
410
+ const tokenData = callResponse.result;
411
+ // accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
412
+ const accessToken = tokenData.accessToken?.$agent_type === "secret"
413
+ ? tokenData.accessToken.value
414
+ : tokenData.accessToken;
335
415
  return jsonResponse({
336
- access_token: tokenString,
337
- token_type: "Bearer",
338
- expires_in: authConfig.tokenTtl,
339
- scope: client.scopes.join(" "),
416
+ access_token: accessToken,
417
+ token_type: tokenData.tokenType ?? "Bearer",
418
+ expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
419
+ scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
340
420
  });
341
421
  }
342
422
  // ──────────────────────────────────────────
@@ -389,6 +469,7 @@ export function createAgentServer(registry, options = {}) {
389
469
  name: agent.config?.name,
390
470
  description: agent.config?.description,
391
471
  supportedActions: agent.config?.supportedActions,
472
+ integration: agent.config?.integration || null,
392
473
  tools: agent.tools
393
474
  .filter((t) => {
394
475
  const tv = t.visibility ?? "internal";
@@ -404,11 +485,381 @@ export function createAgentServer(registry, options = {}) {
404
485
  })),
405
486
  }));
406
487
  }
407
- return addCors(jsonResponse({ jsonrpc: "2.0", id: null, error: { code: -32601, message: `Not found: ${req.method} ${path}` } }, 404));
488
+ // GET /integrations/callback/:provider - OAuth callback
489
+ if (path.startsWith("/integrations/callback/") && req.method === "GET") {
490
+ const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
491
+ if (!provider) {
492
+ return addCors(jsonResponse({ error: "Missing provider" }, 400));
493
+ }
494
+ const url = new URL(req.url);
495
+ const code = url.searchParams.get("code");
496
+ const state = url.searchParams.get("state");
497
+ const oauthError = url.searchParams.get("error");
498
+ const errorDescription = url.searchParams.get("error_description");
499
+ if (oauthError) {
500
+ return new Response(`<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`, { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } });
501
+ }
502
+ if (!code) {
503
+ return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
504
+ }
505
+ // Call handle_oauth_callback tool on @integrations
506
+ try {
507
+ await registry.call({
508
+ action: "execute_tool",
509
+ path: "@integrations",
510
+ tool: "handle_oauth_callback",
511
+ params: { provider, code, state: state ?? undefined },
512
+ context: {
513
+ tenantId: "default",
514
+ agentPath: "@integrations",
515
+ callerId: "oauth_callback",
516
+ callerType: "system",
517
+ },
518
+ });
519
+ // Parse redirect URL from state
520
+ let redirectUrl = "/";
521
+ if (state) {
522
+ try {
523
+ const parsed = JSON.parse(state);
524
+ if (parsed.redirectUrl)
525
+ redirectUrl = parsed.redirectUrl;
526
+ }
527
+ catch { }
528
+ }
529
+ const sep = redirectUrl.includes("?") ? "&" : "?";
530
+ return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
531
+ }
532
+ catch (err) {
533
+ return new Response(`<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`, { status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } });
534
+ }
535
+ }
536
+ // GET /secrets/form/:token - Serve hosted secrets form
537
+ if (path.startsWith("/secrets/form/") && req.method === "GET") {
538
+ const token = path.split("/").pop() ?? "";
539
+ const pending = pendingCollections.get(token);
540
+ if (!pending) {
541
+ return addCors(new Response("Invalid or expired form link", { status: 404 }));
542
+ }
543
+ if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
544
+ pendingCollections.delete(token);
545
+ return addCors(new Response("Form link expired", { status: 410 }));
546
+ }
547
+ const reqUrl = new URL(req.url);
548
+ const baseUrl = resolveBaseUrl(req, reqUrl);
549
+ const html = renderSecretForm(token, pending, baseUrl);
550
+ return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
551
+ }
552
+ // POST /secrets/collect - Submit collected secrets and auto-forward to tool
553
+ if (path === "/secrets/collect" && req.method === "POST") {
554
+ const body = (await req.json());
555
+ const pending = pendingCollections.get(body.token);
556
+ if (!pending) {
557
+ return addCors(jsonResponse({ error: "Invalid or expired collection token" }, 400));
558
+ }
559
+ // One-time use
560
+ pendingCollections.delete(body.token);
561
+ // Check expiry (10 min)
562
+ if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
563
+ return addCors(jsonResponse({ error: "Collection token expired" }, 400));
564
+ }
565
+ // Encrypt secret values and store as refs
566
+ const mergedParams = { ...pending.params };
567
+ for (const [fieldName, value] of Object.entries(body.values)) {
568
+ const fieldDef = pending.fields.find((f) => f.name === fieldName);
569
+ if (fieldDef?.secret && secretStore) {
570
+ // Store encrypted, get ref
571
+ const ownerId = pending.auth?.callerId ?? "anonymous";
572
+ const secretId = await secretStore.store(value, ownerId);
573
+ mergedParams[fieldName] = `secret:${secretId}`;
574
+ }
575
+ else {
576
+ mergedParams[fieldName] = value;
577
+ }
578
+ }
579
+ // Auto-forward to the target tool
580
+ const callRequest = {
581
+ action: "execute_tool",
582
+ path: pending.agent,
583
+ tool: pending.tool,
584
+ params: mergedParams,
585
+ };
586
+ const toolCtx = {
587
+ tenantId: "default",
588
+ agentPath: pending.agent,
589
+ callerId: pending.auth?.callerId ?? "anonymous",
590
+ callerType: pending.auth?.callerType ?? "system",
591
+ };
592
+ const result = await registry.call({
593
+ ...callRequest,
594
+ context: toolCtx,
595
+ });
596
+ return addCors(jsonResponse({ success: true, result }));
597
+ }
598
+ // --- Web pages (plain HTML, served from same server) ---
599
+ const htmlRes = (body) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
600
+ const reqUrl = new URL(req.url);
601
+ const baseUrl = resolveBaseUrl(req, reqUrl);
602
+ const slackConfig = process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
603
+ ? {
604
+ clientId: process.env.SLACK_CLIENT_ID,
605
+ clientSecret: process.env.SLACK_CLIENT_SECRET,
606
+ redirectUri: `${baseUrl}/auth/slack/callback`,
607
+ }
608
+ : null;
609
+ // Helper: read session from cookie
610
+ function getSession(r) {
611
+ const c = r.headers.get("Cookie") || "";
612
+ const m = c.match(/s_session=([^;]+)/);
613
+ if (!m)
614
+ return null;
615
+ try {
616
+ return JSON.parse(Buffer.from(m[1], "base64url").toString());
617
+ }
618
+ catch {
619
+ return null;
620
+ }
621
+ }
622
+ // Helper: generate JWT from client credentials
623
+ async function generateMcpToken() {
624
+ const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
625
+ name: "mcp-" + Date.now(),
626
+ scopes: ["*"],
627
+ } });
628
+ const cid = clientRes?.result?.clientId;
629
+ const csec = clientRes?.result?.clientSecret;
630
+ if (!cid || !csec)
631
+ throw new Error("Failed to create client: " + JSON.stringify(clientRes));
632
+ const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
633
+ method: "POST",
634
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
635
+ body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
636
+ });
637
+ const tokenData = await tokenRes.json();
638
+ if (!tokenData.access_token)
639
+ throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
640
+ return tokenData.access_token;
641
+ }
642
+ // Helper: set session cookie and redirect
643
+ function sessionRedirect(location, session) {
644
+ const data = Buffer.from(JSON.stringify(session)).toString("base64url");
645
+ return new Response(null, {
646
+ status: 302,
647
+ headers: {
648
+ Location: location,
649
+ "Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
650
+ },
651
+ });
652
+ }
653
+ // GET / — login page (or redirect to dashboard if session exists)
654
+ if (path === "/" && req.method === "GET") {
655
+ const session = getSession(req);
656
+ if (session?.token)
657
+ return Response.redirect(`${baseUrl}/dashboard`, 302);
658
+ return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
659
+ }
660
+ // GET /auth/slack — start Slack OAuth
661
+ if (path === "/auth/slack" && req.method === "GET") {
662
+ if (!slackConfig)
663
+ return htmlRes("<h1>Slack OAuth not configured</h1>");
664
+ return Response.redirect(slackAuthUrl(slackConfig), 302);
665
+ }
666
+ // GET /auth/slack/callback — handle Slack OAuth callback
667
+ if (path === "/auth/slack/callback" && req.method === "GET") {
668
+ if (!slackConfig)
669
+ return htmlRes("<h1>Slack OAuth not configured</h1>");
670
+ const authCode = reqUrl.searchParams.get("code");
671
+ const authError = reqUrl.searchParams.get("error");
672
+ if (authError || !authCode)
673
+ return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
674
+ try {
675
+ const tokens = await exchangeSlackCode(authCode, slackConfig);
676
+ const profile = await getSlackProfile(tokens.access_token);
677
+ const teamId = profile["https://slack.com/team_id"] || "";
678
+ const teamName = profile["https://slack.com/team_name"] || "";
679
+ // Check if user already exists
680
+ console.log("[auth] Looking up slack user:", profile.sub, profile.email);
681
+ const existing = await registry.call({
682
+ action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
683
+ params: { provider: "slack", providerUserId: profile.sub },
684
+ });
685
+ console.log("[auth] resolve_identity:", JSON.stringify(existing));
686
+ if (existing?.result?.found && existing?.result?.user?.tenantId) {
687
+ // Returning user — generate token and go to dashboard
688
+ console.log("[auth] Returning user, generating token...");
689
+ const mcpToken = await generateMcpToken();
690
+ return sessionRedirect(`${baseUrl}/dashboard`, {
691
+ userId: existing.result.user.id,
692
+ tenantId: existing.result.user.tenantId,
693
+ email: existing.result.user.email,
694
+ name: existing.result.user.name,
695
+ token: mcpToken,
696
+ });
697
+ }
698
+ // Check if Slack team already has a tenant
699
+ if (teamId) {
700
+ console.log("[auth] Checking tenant_identities for team:", teamId);
701
+ try {
702
+ // Direct DB query via the auth store's underlying connection
703
+ // We'll use a simple fetch to our own MCP endpoint to call @db-connections
704
+ // Actually, simpler: just query the DB directly via the server's context
705
+ // For now, use registry.call to a custom tool or direct SQL
706
+ // Simplest: call @auth list_tenants and check metadata
707
+ // Even simpler: direct SQL via globalThis.fetch to ourselves
708
+ const dbUrl = process.env.DATABASE_URL;
709
+ if (dbUrl) {
710
+ const { default: postgres } = await import("postgres");
711
+ const sql = postgres(dbUrl);
712
+ const rows = await sql `SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
713
+ await sql.end();
714
+ if (rows.length > 0) {
715
+ const existingTenantId = rows[0].tenant_id;
716
+ console.log("[auth] Found existing tenant for team:", existingTenantId);
717
+ // Create user on existing tenant
718
+ const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
719
+ email: profile.email, name: profile.name, tenantId: existingTenantId,
720
+ } });
721
+ const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
722
+ console.log("[auth] Created user on existing tenant:", newUserId);
723
+ // Link identity
724
+ if (newUserId) {
725
+ await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
726
+ userId: newUserId, provider: "slack", providerUserId: profile.sub,
727
+ email: profile.email, name: profile.name,
728
+ metadata: { slackTeamId: teamId, slackTeamName: teamName },
729
+ } });
730
+ }
731
+ // Generate token and go to dashboard
732
+ const mcpToken = await generateMcpToken();
733
+ return sessionRedirect(`${baseUrl}/dashboard`, {
734
+ userId: newUserId, tenantId: existingTenantId,
735
+ email: profile.email, name: profile.name, token: mcpToken,
736
+ });
737
+ }
738
+ }
739
+ }
740
+ catch (e) {
741
+ console.error("[auth] tenant_identity lookup error:", e.message);
742
+ }
743
+ }
744
+ // New user — redirect to setup
745
+ return sessionRedirect(`${baseUrl}/setup`, {
746
+ email: profile.email,
747
+ name: profile.name,
748
+ picture: profile.picture,
749
+ slackUserId: profile.sub,
750
+ slackTeamId: teamId,
751
+ slackTeamName: teamName,
752
+ });
753
+ }
754
+ catch (err) {
755
+ console.error("[auth] callback error:", err);
756
+ return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
757
+ }
758
+ }
759
+ // GET /setup — tenant creation page
760
+ if (path === "/setup" && req.method === "GET") {
761
+ const session = getSession(req);
762
+ if (!session?.email)
763
+ return Response.redirect(`${baseUrl}/`, 302);
764
+ return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
765
+ }
766
+ // POST /setup — create tenant + user + link identity + generate token
767
+ if (path === "/setup" && req.method === "POST") {
768
+ try {
769
+ const body = await req.json();
770
+ const session = getSession(req);
771
+ console.log("[setup] body:", JSON.stringify(body), "session:", JSON.stringify(session));
772
+ // 1. Create tenant
773
+ const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } });
774
+ const tenantId = tenantRes?.result?.tenantId;
775
+ if (!tenantId)
776
+ return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
777
+ console.log("[setup] tenant created:", tenantId);
778
+ // 2. Create user
779
+ const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } });
780
+ const userId = userRes?.result?.id || userRes?.result?.user?.id;
781
+ console.log("[setup] user created:", userId);
782
+ // 2b. Link tenant to Slack team
783
+ if (session?.slackTeamId) {
784
+ try {
785
+ const dbUrl = process.env.DATABASE_URL;
786
+ if (dbUrl) {
787
+ const { default: postgres } = await import("postgres");
788
+ const sql = postgres(dbUrl);
789
+ const id = "ti_" + Math.random().toString(36).slice(2, 14);
790
+ await sql `INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
791
+ await sql.end();
792
+ console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
793
+ }
794
+ }
795
+ catch (e) {
796
+ console.error("[setup] tenant_identity insert error:", e.message);
797
+ }
798
+ }
799
+ // 3. Link Slack identity
800
+ if (session?.slackUserId && userId) {
801
+ console.log("[setup] linking slack identity:", session.slackUserId);
802
+ const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
803
+ userId,
804
+ provider: "slack",
805
+ providerUserId: session.slackUserId,
806
+ email: body.email,
807
+ name: session.name,
808
+ metadata: { slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system"
809
+ } });
810
+ console.log("[setup] link_identity result:", JSON.stringify(linkRes));
811
+ }
812
+ // 4. Generate MCP token
813
+ const mcpToken = await generateMcpToken();
814
+ console.log("[setup] token generated, length:", mcpToken.length);
815
+ return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
816
+ }
817
+ catch (err) {
818
+ console.error("[setup] error:", err);
819
+ return addCors(jsonResponse({ error: err.message }, 400));
820
+ }
821
+ }
822
+ // POST /logout — clear session
823
+ if (path === "/logout" && req.method === "POST") {
824
+ return new Response(null, {
825
+ status: 302,
826
+ headers: {
827
+ Location: `${baseUrl}/`,
828
+ "Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
829
+ },
830
+ });
831
+ }
832
+ // GET /dashboard — show MCP URL and setup instructions
833
+ if (path === "/dashboard" && req.method === "GET") {
834
+ const session = getSession(req);
835
+ let token = session?.token || reqUrl.searchParams.get("token") || "";
836
+ if (!token)
837
+ return Response.redirect(`${baseUrl}/`, 302);
838
+ // Persist token in cookie
839
+ const sessData = Buffer.from(JSON.stringify({ ...session, token })).toString("base64url");
840
+ return new Response(renderDashboardPage(baseUrl, token, session || undefined), {
841
+ headers: {
842
+ "Content-Type": "text/html; charset=utf-8",
843
+ "Set-Cookie": `s_session=${sessData}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
844
+ },
845
+ });
846
+ }
847
+ return addCors(jsonResponse({
848
+ jsonrpc: "2.0",
849
+ id: null,
850
+ error: {
851
+ code: -32601,
852
+ message: `Not found: ${req.method} ${path}`,
853
+ },
854
+ }, 404));
408
855
  }
409
856
  catch (err) {
410
857
  console.error("[server] Request error:", err);
411
- return addCors(jsonResponse({ jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal error" } }, 500));
858
+ return addCors(jsonResponse({
859
+ jsonrpc: "2.0",
860
+ id: null,
861
+ error: { code: -32603, message: "Internal error" },
862
+ }, 500));
412
863
  }
413
864
  }
414
865
  // ──────────────────────────────────────────
@@ -421,14 +872,14 @@ export function createAgentServer(registry, options = {}) {
421
872
  serverInstance = Bun.serve({ port, hostname, fetch });
422
873
  serverUrl = `http://${hostname}:${port}${basePath}`;
423
874
  console.log(`Agent server running at ${serverUrl}`);
424
- console.log(` POST / - MCP JSON-RPC endpoint`);
425
- console.log(` POST /mcp - MCP JSON-RPC endpoint (alias)`);
426
- console.log(` GET /health - Health check`);
875
+ console.log(" POST / - MCP JSON-RPC endpoint");
876
+ console.log(" POST /mcp - MCP JSON-RPC endpoint (alias)");
877
+ console.log(" GET /health - Health check");
427
878
  if (authConfig) {
428
- console.log(` POST /oauth/token - OAuth2 token endpoint`);
879
+ console.log(" POST /oauth/token - OAuth2 token endpoint");
429
880
  console.log(" Auth: enabled");
430
881
  }
431
- console.log(` MCP tools: call_agent, list_agents`);
882
+ console.log(" MCP tools: call_agent, list_agents");
432
883
  },
433
884
  async stop() {
434
885
  if (serverInstance) {