@slashfi/agents-sdk 0.5.0 → 0.7.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 (54) hide show
  1. package/dist/agent-definitions/auth.d.ts.map +1 -1
  2. package/dist/agent-definitions/auth.js +16 -3
  3. package/dist/agent-definitions/auth.js.map +1 -1
  4. package/dist/agent-definitions/integrations.d.ts +162 -0
  5. package/dist/agent-definitions/integrations.d.ts.map +1 -0
  6. package/dist/agent-definitions/integrations.js +861 -0
  7. package/dist/agent-definitions/integrations.js.map +1 -0
  8. package/dist/agent-definitions/secrets.d.ts +26 -3
  9. package/dist/agent-definitions/secrets.d.ts.map +1 -1
  10. package/dist/agent-definitions/secrets.js +34 -28
  11. package/dist/agent-definitions/secrets.js.map +1 -1
  12. package/dist/agent-definitions/users.d.ts +80 -0
  13. package/dist/agent-definitions/users.d.ts.map +1 -0
  14. package/dist/agent-definitions/users.js +397 -0
  15. package/dist/agent-definitions/users.js.map +1 -0
  16. package/dist/crypto.d.ts.map +1 -1
  17. package/dist/crypto.js.map +1 -1
  18. package/dist/define.d.ts +6 -1
  19. package/dist/define.d.ts.map +1 -1
  20. package/dist/define.js +1 -0
  21. package/dist/define.js.map +1 -1
  22. package/dist/index.d.ts +8 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/jwt.d.ts.map +1 -1
  27. package/dist/jwt.js.map +1 -1
  28. package/dist/server.d.ts +28 -1
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +477 -26
  31. package/dist/server.js.map +1 -1
  32. package/dist/slack-oauth.d.ts +27 -0
  33. package/dist/slack-oauth.d.ts.map +1 -0
  34. package/dist/slack-oauth.js +48 -0
  35. package/dist/slack-oauth.js.map +1 -0
  36. package/dist/types.d.ts +66 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/web-pages.d.ts +8 -0
  39. package/dist/web-pages.d.ts.map +1 -0
  40. package/dist/web-pages.js +169 -0
  41. package/dist/web-pages.js.map +1 -0
  42. package/package.json +2 -1
  43. package/src/agent-definitions/auth.ts +37 -14
  44. package/src/agent-definitions/integrations.ts +1209 -0
  45. package/src/agent-definitions/secrets.ts +85 -36
  46. package/src/agent-definitions/users.ts +533 -0
  47. package/src/crypto.ts +3 -1
  48. package/src/define.ts +8 -0
  49. package/src/index.ts +57 -3
  50. package/src/jwt.ts +7 -5
  51. package/src/server.ts +565 -33
  52. package/src/slack-oauth.ts +66 -0
  53. package/src/types.ts +83 -0
  54. package/src/web-pages.ts +178 -0
package/src/server.ts CHANGED
@@ -26,10 +26,22 @@
26
26
  */
27
27
 
28
28
  import type { AuthStore } from "./agent-definitions/auth.js";
29
+ import {
30
+ type SecretStore,
31
+ processSecretParams,
32
+ } from "./agent-definitions/secrets.js";
33
+ import { verifyJwt } from "./jwt.js";
29
34
  import type { AgentRegistry } from "./registry.js";
30
35
  import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
31
- import { verifyJwt } from "./jwt.js";
32
- import { type SecretStore, processSecretParams } from "./agent-definitions/secrets.js";
36
+ import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
37
+ import { slackAuthUrl, exchangeSlackCode, getSlackProfile, type SlackOAuthConfig } from "./slack-oauth.js";
38
+
39
+
40
+ function resolveBaseUrl(req: Request, url: URL): string {
41
+ const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
42
+ const host = req.headers.get("x-forwarded-host") || url.host;
43
+ return `${proto}://${host}`;
44
+ }
33
45
 
34
46
  // ============================================
35
47
  // Server Types
@@ -99,6 +111,61 @@ interface ResolvedAuth {
99
111
  isRoot: boolean;
100
112
  }
101
113
 
114
+
115
+ // ============================================
116
+ // Secrets Collection (one-time tokens)
117
+ // ============================================
118
+
119
+
120
+ function escHtml(s: string): string {
121
+ return s.replace(/&/g,"\&amp;").replace(/</g,"\&lt;").replace(/>/g,"\&gt;").replace(/"/g,"\&quot;");
122
+ }
123
+
124
+ function renderSecretForm(token: string, pending: PendingCollection, baseUrl: string): string {
125
+ const fields = pending.fields.map(f => `
126
+ <div class="field">
127
+ <label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
128
+ ${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
129
+ <input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
130
+ </div>`).join("");
131
+
132
+ return `<!DOCTYPE html>
133
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
134
+ <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>
135
+ <div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
136
+ <p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
137
+ <div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
138
+ <form id="f">${fields}<button type="submit">Submit Securely</button></form>
139
+ <p class="footer">Expires in 10 minutes</p></div>
140
+ <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>
141
+ <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>`;
142
+ }
143
+
144
+ export interface PendingCollection {
145
+ /** Partial params already provided by agent */
146
+ params: Record<string, unknown>;
147
+ /** Target agent + tool to call after collection */
148
+ agent: string;
149
+ tool: string;
150
+ /** Auth context from original request */
151
+ auth: ResolvedAuth | null;
152
+ /** Fields the form needs to collect */
153
+ fields: Array<{ name: string; description?: string; secret: boolean; required: boolean }>;
154
+ /** Created timestamp for expiry */
155
+ createdAt: number;
156
+ }
157
+
158
+ export const pendingCollections = new Map<string, PendingCollection>();
159
+
160
+ export function generateCollectionToken(): string {
161
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
162
+ let token = "sc_";
163
+ for (let i = 0; i < 32; i++) {
164
+ token += chars[Math.floor(Math.random() * chars.length)];
165
+ }
166
+ return token;
167
+ }
168
+
102
169
  // ============================================
103
170
  // Helpers
104
171
  // ============================================
@@ -129,7 +196,11 @@ function jsonRpcError(
129
196
  message: string,
130
197
  data?: unknown,
131
198
  ): JsonRpcResponse {
132
- return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined && { data }) } };
199
+ return {
200
+ jsonrpc: "2.0",
201
+ id,
202
+ error: { code, message, ...(data !== undefined && { data }) },
203
+ };
133
204
  }
134
205
 
135
206
  /** Wrap a value as MCP tool result content */
@@ -138,7 +209,8 @@ function mcpResult(value: unknown, isError = false) {
138
209
  content: [
139
210
  {
140
211
  type: "text",
141
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
212
+ text:
213
+ typeof value === "string" ? value : JSON.stringify(value, null, 2),
142
214
  },
143
215
  ],
144
216
  ...(isError && { isError: true }),
@@ -178,7 +250,12 @@ async function resolveAuth(
178
250
  if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
179
251
 
180
252
  if (credential === authConfig.rootKey) {
181
- return { callerId: "root", callerType: "system", scopes: ["*"], isRoot: true };
253
+ return {
254
+ callerId: "root",
255
+ callerType: "system",
256
+ scopes: ["*"],
257
+ isRoot: true,
258
+ };
182
259
  }
183
260
 
184
261
  // Try JWT verification first (stateless)
@@ -190,7 +267,12 @@ async function resolveAuth(
190
267
  try {
191
268
  const payloadB64 = parts[1];
192
269
  const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
193
- const payload = JSON.parse(atob(padded)) as { sub?: string; name?: string; scopes?: string[]; exp?: number };
270
+ const payload = JSON.parse(atob(padded)) as {
271
+ sub?: string;
272
+ name?: string;
273
+ scopes?: string[];
274
+ exp?: number;
275
+ };
194
276
 
195
277
  if (payload.sub) {
196
278
  // Look up client to get the signing secret (secret hash)
@@ -225,8 +307,13 @@ async function resolveAuth(
225
307
  };
226
308
  }
227
309
 
228
- function canSeeAgent(agent: AgentDefinition, auth: ResolvedAuth | null): boolean {
229
- const visibility = ((agent as any).visibility ?? agent.config?.visibility ?? "internal") as Visibility;
310
+ function canSeeAgent(
311
+ agent: AgentDefinition,
312
+ auth: ResolvedAuth | null,
313
+ ): boolean {
314
+ const visibility = ((agent as any).visibility ??
315
+ agent.config?.visibility ??
316
+ "internal") as Visibility;
230
317
  if (auth?.isRoot) return true;
231
318
  if (visibility === "public") return true;
232
319
  if (visibility === "internal" && auth) return true;
@@ -346,7 +433,7 @@ export function createAgentServer(
346
433
  const result = await handleToolCall(name, args ?? {}, auth);
347
434
  return jsonRpcSuccess(request.id, result);
348
435
  } catch (err) {
349
- console.error("[server] Request error:", err);
436
+ console.error("[server] Request error:", err);
350
437
  return jsonRpcSuccess(
351
438
  request.id,
352
439
  mcpResult(
@@ -392,6 +479,7 @@ export function createAgentServer(
392
479
  }
393
480
 
394
481
  // Process secret params: resolve refs, store raw secrets
482
+ // Auto-resolve secret:xxx refs in tool params before execution
395
483
  if ((req as any).params && secretStore) {
396
484
  const ownerId = auth?.callerId ?? "anonymous";
397
485
  // Find the tool schema to check for secret: true fields
@@ -422,6 +510,7 @@ export function createAgentServer(
422
510
  name: agent.config?.name,
423
511
  description: agent.config?.description,
424
512
  supportedActions: agent.config?.supportedActions,
513
+ integration: agent.config?.integration || null,
425
514
  tools: agent.tools
426
515
  .filter((t) => {
427
516
  const tv = t.visibility ?? "internal";
@@ -469,42 +558,73 @@ export function createAgentServer(
469
558
 
470
559
  if (grantType !== "client_credentials") {
471
560
  return jsonResponse(
472
- { error: "unsupported_grant_type", error_description: "Only client_credentials is supported" },
561
+ {
562
+ error: "unsupported_grant_type",
563
+ error_description: "Only client_credentials is supported",
564
+ },
473
565
  400,
474
566
  );
475
567
  }
476
568
 
477
569
  if (!clientId || !clientSecret) {
478
570
  return jsonResponse(
479
- { error: "invalid_request", error_description: "Missing client_id or client_secret" },
571
+ {
572
+ error: "invalid_request",
573
+ error_description: "Missing client_id or client_secret",
574
+ },
480
575
  400,
481
576
  );
482
577
  }
483
578
 
484
- const client = await authConfig.store.validateClient(clientId, clientSecret);
579
+ const client = await authConfig.store.validateClient(
580
+ clientId,
581
+ clientSecret,
582
+ );
485
583
  if (!client) {
486
584
  return jsonResponse(
487
- { error: "invalid_client", error_description: "Invalid client credentials" },
585
+ {
586
+ error: "invalid_client",
587
+ error_description: "Invalid client credentials",
588
+ },
488
589
  401,
489
590
  );
490
591
  }
491
592
 
492
- const tokenString = `at_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
493
- const now = Date.now();
593
+ // Delegate to @auth agent's token tool which generates proper JWTs
594
+ const tokenResult = await registry.call({
595
+ action: "execute_tool",
596
+ path: "@auth",
597
+ tool: "token",
598
+ params: {
599
+ grantType: "client_credentials",
600
+ clientId,
601
+ clientSecret,
602
+ },
603
+ context: {
604
+ tenantId: "default",
605
+ agentPath: "@auth",
606
+ callerId: "oauth_endpoint",
607
+ callerType: "system",
608
+ },
609
+ } as any);
494
610
 
495
- await authConfig.store.storeToken({
496
- token: tokenString,
497
- clientId: client.clientId,
498
- scopes: client.scopes,
499
- issuedAt: now,
500
- expiresAt: now + authConfig.tokenTtl * 1000,
501
- });
611
+ // Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
612
+ const callResponse = tokenResult as any;
613
+ if (!callResponse.success) {
614
+ return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
615
+ }
616
+ const tokenData = callResponse.result;
617
+
618
+ // accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
619
+ const accessToken = tokenData.accessToken?.$agent_type === "secret"
620
+ ? tokenData.accessToken.value
621
+ : tokenData.accessToken;
502
622
 
503
623
  return jsonResponse({
504
- access_token: tokenString,
505
- token_type: "Bearer",
506
- expires_in: authConfig.tokenTtl,
507
- scope: client.scopes.join(" "),
624
+ access_token: accessToken,
625
+ token_type: tokenData.tokenType ?? "Bearer",
626
+ expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
627
+ scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
508
628
  });
509
629
  }
510
630
 
@@ -566,6 +686,7 @@ export function createAgentServer(
566
686
  name: agent.config?.name,
567
687
  description: agent.config?.description,
568
688
  supportedActions: agent.config?.supportedActions,
689
+ integration: agent.config?.integration || null,
569
690
  tools: agent.tools
570
691
  .filter((t) => {
571
692
  const tv = t.visibility ?? "internal";
@@ -580,12 +701,423 @@ export function createAgentServer(
580
701
  );
581
702
  }
582
703
 
583
- return addCors(jsonResponse({ jsonrpc: "2.0", id: null, error: { code: -32601, message: `Not found: ${req.method} ${path}` } }, 404));
704
+
705
+ // GET /integrations/callback/:provider - OAuth callback
706
+ if (path.startsWith("/integrations/callback/") && req.method === "GET") {
707
+ const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
708
+ if (!provider) {
709
+ return addCors(jsonResponse({ error: "Missing provider" }, 400));
710
+ }
711
+
712
+ const url = new URL(req.url);
713
+ const code = url.searchParams.get("code");
714
+ const state = url.searchParams.get("state");
715
+ const oauthError = url.searchParams.get("error");
716
+ const errorDescription = url.searchParams.get("error_description");
717
+
718
+ if (oauthError) {
719
+ return new Response(
720
+ `<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`,
721
+ { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } },
722
+ );
723
+ }
724
+
725
+ if (!code) {
726
+ return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
727
+ }
728
+
729
+ // Call handle_oauth_callback tool on @integrations
730
+ try {
731
+ await registry.call({
732
+ action: "execute_tool",
733
+ path: "@integrations",
734
+ tool: "handle_oauth_callback",
735
+ params: { provider, code, state: state ?? undefined },
736
+ context: {
737
+ tenantId: "default",
738
+ agentPath: "@integrations",
739
+ callerId: "oauth_callback",
740
+ callerType: "system",
741
+ },
742
+ } as any);
743
+
744
+ // Parse redirect URL from state
745
+ let redirectUrl = "/";
746
+ if (state) {
747
+ try {
748
+ const parsed = JSON.parse(state);
749
+ if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
750
+ } catch {}
751
+ }
752
+
753
+ const sep = redirectUrl.includes("?") ? "&" : "?";
754
+ return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
755
+ } catch (err) {
756
+ return new Response(
757
+ `<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`,
758
+ { status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } },
759
+ );
760
+ }
761
+ }
762
+
763
+
764
+ // GET /secrets/form/:token - Serve hosted secrets form
765
+ if (path.startsWith("/secrets/form/") && req.method === "GET") {
766
+ const token = path.split("/").pop() ?? "";
767
+ const pending = pendingCollections.get(token);
768
+ if (!pending) {
769
+ return addCors(new Response("Invalid or expired form link", { status: 404 }));
770
+ }
771
+ if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
772
+ pendingCollections.delete(token);
773
+ return addCors(new Response("Form link expired", { status: 410 }));
774
+ }
775
+ const reqUrl = new URL(req.url); const baseUrl = resolveBaseUrl(req, reqUrl);
776
+ const html = renderSecretForm(token, pending, baseUrl);
777
+ return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
778
+ }
779
+
780
+ // POST /secrets/collect - Submit collected secrets and auto-forward to tool
781
+ if (path === "/secrets/collect" && req.method === "POST") {
782
+ const body = (await req.json()) as {
783
+ token: string;
784
+ values: Record<string, string>;
785
+ };
786
+
787
+ const pending = pendingCollections.get(body.token);
788
+ if (!pending) {
789
+ return addCors(
790
+ jsonResponse({ error: "Invalid or expired collection token" }, 400),
791
+ );
792
+ }
793
+
794
+ // One-time use
795
+ pendingCollections.delete(body.token);
796
+
797
+ // Check expiry (10 min)
798
+ if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
799
+ return addCors(
800
+ jsonResponse({ error: "Collection token expired" }, 400),
801
+ );
802
+ }
803
+
804
+ // Encrypt secret values and store as refs
805
+ const mergedParams = { ...pending.params };
806
+ for (const [fieldName, value] of Object.entries(body.values)) {
807
+ const fieldDef = pending.fields.find((f) => f.name === fieldName);
808
+ if (fieldDef?.secret && secretStore) {
809
+ // Store encrypted, get ref
810
+ const ownerId = pending.auth?.callerId ?? "anonymous";
811
+ const secretId = await secretStore.store(value, ownerId);
812
+ mergedParams[fieldName] = `secret:${secretId}`;
813
+ } else {
814
+ mergedParams[fieldName] = value;
815
+ }
816
+ }
817
+
818
+ // Auto-forward to the target tool
819
+ const callRequest = {
820
+ action: "execute_tool" as const,
821
+ path: pending.agent,
822
+ tool: pending.tool,
823
+ params: mergedParams,
824
+ };
825
+
826
+ const toolCtx = {
827
+ tenantId: "default",
828
+ agentPath: pending.agent,
829
+ callerId: pending.auth?.callerId ?? "anonymous",
830
+ callerType: pending.auth?.callerType ?? ("system" as const),
831
+ };
832
+
833
+ const result = await registry.call({
834
+ ...callRequest,
835
+ context: toolCtx,
836
+ } as any);
837
+
838
+ return addCors(jsonResponse({ success: true, result }));
839
+ }
840
+
841
+
842
+ // --- Web pages (plain HTML, served from same server) ---
843
+ const htmlRes = (body: string) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
844
+ const reqUrl = new URL(req.url);
845
+ const baseUrl = resolveBaseUrl(req, reqUrl);
846
+
847
+ const slackConfig: SlackOAuthConfig | null =
848
+ process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
849
+ ? {
850
+ clientId: process.env.SLACK_CLIENT_ID,
851
+ clientSecret: process.env.SLACK_CLIENT_SECRET,
852
+ redirectUri: `${baseUrl}/auth/slack/callback`,
853
+ }
854
+ : null;
855
+
856
+ // Helper: read session from cookie
857
+ function getSession(r: Request): Record<string, any> | null {
858
+ const c = r.headers.get("Cookie") || "";
859
+ const m = c.match(/s_session=([^;]+)/);
860
+ if (!m) return null;
861
+ try { return JSON.parse(Buffer.from(m[1], "base64url").toString()); }
862
+ catch { return null; }
863
+ }
864
+
865
+ // Helper: generate JWT from client credentials
866
+ async function generateMcpToken(): Promise<string> {
867
+ const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
868
+ name: "mcp-" + Date.now(),
869
+ scopes: ["*"],
870
+ }} as any) as any;
871
+ const cid = clientRes?.result?.clientId;
872
+ const csec = clientRes?.result?.clientSecret;
873
+ if (!cid || !csec) throw new Error("Failed to create client: " + JSON.stringify(clientRes));
874
+
875
+ const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
876
+ method: "POST",
877
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
878
+ body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
879
+ });
880
+ const tokenData = await tokenRes.json() as any;
881
+ if (!tokenData.access_token) throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
882
+ return tokenData.access_token;
883
+ }
884
+
885
+ // Helper: set session cookie and redirect
886
+ function sessionRedirect(location: string, session: Record<string, any>): Response {
887
+ const data = Buffer.from(JSON.stringify(session)).toString("base64url");
888
+ return new Response(null, {
889
+ status: 302,
890
+ headers: {
891
+ Location: location,
892
+ "Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
893
+ },
894
+ });
895
+ }
896
+
897
+ // GET / — login page (or redirect to dashboard if session exists)
898
+ if (path === "/" && req.method === "GET") {
899
+ const session = getSession(req);
900
+ if (session?.token) return Response.redirect(`${baseUrl}/dashboard`, 302);
901
+ return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
902
+ }
903
+
904
+ // GET /auth/slack — start Slack OAuth
905
+ if (path === "/auth/slack" && req.method === "GET") {
906
+ if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
907
+ return Response.redirect(slackAuthUrl(slackConfig), 302);
908
+ }
909
+
910
+ // GET /auth/slack/callback — handle Slack OAuth callback
911
+ if (path === "/auth/slack/callback" && req.method === "GET") {
912
+ if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
913
+ const authCode = reqUrl.searchParams.get("code");
914
+ const authError = reqUrl.searchParams.get("error");
915
+ if (authError || !authCode) return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
916
+
917
+ try {
918
+ const tokens = await exchangeSlackCode(authCode, slackConfig);
919
+ const profile = await getSlackProfile(tokens.access_token);
920
+ const teamId = profile["https://slack.com/team_id"] || "";
921
+ const teamName = profile["https://slack.com/team_name"] || "";
922
+
923
+ // Check if user already exists
924
+ console.log("[auth] Looking up slack user:", profile.sub, profile.email);
925
+ const existing = await registry.call({
926
+ action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
927
+ params: { provider: "slack", providerUserId: profile.sub },
928
+ } as any) as any;
929
+ console.log("[auth] resolve_identity:", JSON.stringify(existing));
930
+
931
+ if (existing?.result?.found && existing?.result?.user?.tenantId) {
932
+ // Returning user — generate token and go to dashboard
933
+ console.log("[auth] Returning user, generating token...");
934
+ const mcpToken = await generateMcpToken();
935
+ return sessionRedirect(`${baseUrl}/dashboard`, {
936
+ userId: existing.result.user.id,
937
+ tenantId: existing.result.user.tenantId,
938
+ email: existing.result.user.email,
939
+ name: existing.result.user.name,
940
+ token: mcpToken,
941
+ });
942
+ }
943
+
944
+ // Check if Slack team already has a tenant
945
+ if (teamId) {
946
+ console.log("[auth] Checking tenant_identities for team:", teamId);
947
+ try {
948
+ // Direct DB query via the auth store's underlying connection
949
+ // We'll use a simple fetch to our own MCP endpoint to call @db-connections
950
+ // Actually, simpler: just query the DB directly via the server's context
951
+ // For now, use registry.call to a custom tool or direct SQL
952
+ // Simplest: call @auth list_tenants and check metadata
953
+ // Even simpler: direct SQL via globalThis.fetch to ourselves
954
+ const dbUrl = process.env.DATABASE_URL;
955
+ if (dbUrl) {
956
+ const { default: postgres } = await import("postgres");
957
+ const sql = postgres(dbUrl);
958
+ const rows = await sql`SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
959
+ await sql.end();
960
+ if (rows.length > 0) {
961
+ const existingTenantId = rows[0].tenant_id;
962
+ console.log("[auth] Found existing tenant for team:", existingTenantId);
963
+
964
+ // Create user on existing tenant
965
+ const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
966
+ email: profile.email, name: profile.name, tenantId: existingTenantId,
967
+ }} as any) as any;
968
+ const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
969
+ console.log("[auth] Created user on existing tenant:", newUserId);
970
+
971
+ // Link identity
972
+ if (newUserId) {
973
+ await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
974
+ userId: newUserId, provider: "slack", providerUserId: profile.sub,
975
+ email: profile.email, name: profile.name,
976
+ metadata: { slackTeamId: teamId, slackTeamName: teamName },
977
+ }} as any);
978
+ }
979
+
980
+ // Generate token and go to dashboard
981
+ const mcpToken = await generateMcpToken();
982
+ return sessionRedirect(`${baseUrl}/dashboard`, {
983
+ userId: newUserId, tenantId: existingTenantId,
984
+ email: profile.email, name: profile.name, token: mcpToken,
985
+ });
986
+ }
987
+ }
988
+ } catch (e: any) {
989
+ console.error("[auth] tenant_identity lookup error:", e.message);
990
+ }
991
+ }
992
+
993
+ // New user — redirect to setup
994
+ return sessionRedirect(`${baseUrl}/setup`, {
995
+ email: profile.email,
996
+ name: profile.name,
997
+ picture: profile.picture,
998
+ slackUserId: profile.sub,
999
+ slackTeamId: teamId,
1000
+ slackTeamName: teamName,
1001
+ });
1002
+ } catch (err: any) {
1003
+ console.error("[auth] callback error:", err);
1004
+ return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
1005
+ }
1006
+ }
1007
+
1008
+ // GET /setup — tenant creation page
1009
+ if (path === "/setup" && req.method === "GET") {
1010
+ const session = getSession(req);
1011
+ if (!session?.email) return Response.redirect(`${baseUrl}/`, 302);
1012
+ return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
1013
+ }
1014
+
1015
+ // POST /setup — create tenant + user + link identity + generate token
1016
+ if (path === "/setup" && req.method === "POST") {
1017
+ try {
1018
+ const body = await req.json() as { email?: string; tenant?: string; name?: string };
1019
+ const session = getSession(req);
1020
+ console.log("[setup] body:", JSON.stringify(body), "session:", JSON.stringify(session));
1021
+
1022
+ // 1. Create tenant
1023
+ const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } } as any) as any;
1024
+ const tenantId = tenantRes?.result?.tenantId;
1025
+ if (!tenantId) return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
1026
+ console.log("[setup] tenant created:", tenantId);
1027
+
1028
+ // 2. Create user
1029
+ 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;
1030
+ const userId = userRes?.result?.id || userRes?.result?.user?.id;
1031
+ console.log("[setup] user created:", userId);
1032
+
1033
+ // 2b. Link tenant to Slack team
1034
+ if (session?.slackTeamId) {
1035
+ try {
1036
+ const dbUrl = process.env.DATABASE_URL;
1037
+ if (dbUrl) {
1038
+ const { default: postgres } = await import("postgres");
1039
+ const sql = postgres(dbUrl);
1040
+ const id = "ti_" + Math.random().toString(36).slice(2, 14);
1041
+ await sql`INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
1042
+ await sql.end();
1043
+ console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
1044
+ }
1045
+ } catch (e: any) { console.error("[setup] tenant_identity insert error:", e.message); }
1046
+ }
1047
+
1048
+ // 3. Link Slack identity
1049
+ if (session?.slackUserId && userId) {
1050
+ console.log("[setup] linking slack identity:", session.slackUserId);
1051
+ const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
1052
+ userId,
1053
+ provider: "slack",
1054
+ providerUserId: session.slackUserId,
1055
+ email: body.email,
1056
+ name: session.name,
1057
+ metadata:{ slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system" }} as any);
1058
+ console.log("[setup] link_identity result:", JSON.stringify(linkRes));
1059
+ }
1060
+
1061
+ // 4. Generate MCP token
1062
+ const mcpToken = await generateMcpToken();
1063
+ console.log("[setup] token generated, length:", mcpToken.length);
1064
+
1065
+ return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
1066
+ } catch (err: any) {
1067
+ console.error("[setup] error:", err);
1068
+ return addCors(jsonResponse({ error: err.message }, 400));
1069
+ }
1070
+ }
1071
+
1072
+ // POST /logout — clear session
1073
+ if (path === "/logout" && req.method === "POST") {
1074
+ return new Response(null, {
1075
+ status: 302,
1076
+ headers: {
1077
+ Location: `${baseUrl}/`,
1078
+ "Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
1079
+ },
1080
+ });
1081
+ }
1082
+
1083
+ // GET /dashboard — show MCP URL and setup instructions
1084
+ if (path === "/dashboard" && req.method === "GET") {
1085
+ const session = getSession(req);
1086
+ let token = session?.token || reqUrl.searchParams.get("token") || "";
1087
+ if (!token) return Response.redirect(`${baseUrl}/`, 302);
1088
+
1089
+ // Persist token in cookie
1090
+ const sessData = Buffer.from(JSON.stringify({ ...session, token })).toString("base64url");
1091
+ return new Response(renderDashboardPage(baseUrl, token, session || undefined), {
1092
+ headers: {
1093
+ "Content-Type": "text/html; charset=utf-8",
1094
+ "Set-Cookie": `s_session=${sessData}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
1095
+ },
1096
+ });
1097
+ }
1098
+
1099
+ return addCors(
1100
+ jsonResponse(
1101
+ {
1102
+ jsonrpc: "2.0",
1103
+ id: null,
1104
+ error: {
1105
+ code: -32601,
1106
+ message: `Not found: ${req.method} ${path}`,
1107
+ },
1108
+ },
1109
+ 404,
1110
+ ),
1111
+ );
584
1112
  } catch (err) {
585
1113
  console.error("[server] Request error:", err);
586
1114
  return addCors(
587
1115
  jsonResponse(
588
- { jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal error" } },
1116
+ {
1117
+ jsonrpc: "2.0",
1118
+ id: null,
1119
+ error: { code: -32603, message: "Internal error" },
1120
+ },
589
1121
  500,
590
1122
  ),
591
1123
  );
@@ -604,14 +1136,14 @@ export function createAgentServer(
604
1136
  serverUrl = `http://${hostname}:${port}${basePath}`;
605
1137
 
606
1138
  console.log(`Agent server running at ${serverUrl}`);
607
- console.log(` POST / - MCP JSON-RPC endpoint`);
608
- console.log(` POST /mcp - MCP JSON-RPC endpoint (alias)`);
609
- console.log(` GET /health - Health check`);
1139
+ console.log(" POST / - MCP JSON-RPC endpoint");
1140
+ console.log(" POST /mcp - MCP JSON-RPC endpoint (alias)");
1141
+ console.log(" GET /health - Health check");
610
1142
  if (authConfig) {
611
- console.log(` POST /oauth/token - OAuth2 token endpoint`);
1143
+ console.log(" POST /oauth/token - OAuth2 token endpoint");
612
1144
  console.log(" Auth: enabled");
613
1145
  }
614
- console.log(` MCP tools: call_agent, list_agents`);
1146
+ console.log(" MCP tools: call_agent, list_agents");
615
1147
  },
616
1148
 
617
1149
  async stop(): Promise<void> {