@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.
- package/dist/{auth.d.ts → agent-definitions/auth.d.ts} +36 -2
- package/dist/agent-definitions/auth.d.ts.map +1 -0
- package/dist/{auth.js → agent-definitions/auth.js} +69 -8
- package/dist/agent-definitions/auth.js.map +1 -0
- package/dist/agent-definitions/integrations.d.ts +162 -0
- package/dist/agent-definitions/integrations.d.ts.map +1 -0
- package/dist/agent-definitions/integrations.js +861 -0
- package/dist/agent-definitions/integrations.js.map +1 -0
- package/dist/agent-definitions/secrets.d.ts +51 -0
- package/dist/agent-definitions/secrets.d.ts.map +1 -0
- package/dist/agent-definitions/secrets.js +165 -0
- package/dist/agent-definitions/secrets.js.map +1 -0
- package/dist/agent-definitions/users.d.ts +80 -0
- package/dist/agent-definitions/users.d.ts.map +1 -0
- package/dist/agent-definitions/users.js +397 -0
- package/dist/agent-definitions/users.js.map +1 -0
- package/dist/crypto.d.ts +14 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +40 -0
- package/dist/crypto.js.map +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +1 -0
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +2 -0
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/server.d.ts +28 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +478 -27
- package/dist/server.js.map +1 -1
- package/dist/slack-oauth.d.ts +27 -0
- package/dist/slack-oauth.d.ts.map +1 -0
- package/dist/slack-oauth.js +48 -0
- package/dist/slack-oauth.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web-pages.d.ts +8 -0
- package/dist/web-pages.d.ts.map +1 -0
- package/dist/web-pages.js +169 -0
- package/dist/web-pages.js.map +1 -0
- package/package.json +2 -1
- package/src/{auth.ts → agent-definitions/auth.ts} +134 -15
- package/src/agent-definitions/integrations.ts +1209 -0
- package/src/agent-definitions/secrets.ts +241 -0
- package/src/agent-definitions/users.ts +533 -0
- package/src/crypto.ts +71 -0
- package/src/define.ts +8 -0
- package/src/index.ts +62 -4
- package/src/jwt.ts +9 -5
- package/src/server.ts +567 -35
- package/src/slack-oauth.ts +66 -0
- package/src/types.ts +83 -0
- package/src/web-pages.ts +178 -0
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/secrets.d.ts +0 -44
- package/dist/secrets.d.ts.map +0 -1
- package/dist/secrets.js +0 -106
- package/dist/secrets.js.map +0 -1
- package/src/secrets.ts +0 -154
package/src/server.ts
CHANGED
|
@@ -25,11 +25,23 @@
|
|
|
25
25
|
* - Recognizes the root key for admin access
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import type { AuthStore } from "./auth.js";
|
|
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 {
|
|
32
|
-
import {
|
|
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,"\&").replace(/</g,"\<").replace(/>/g,"\>").replace(/"/g,"\"");
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
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(
|
|
229
|
-
|
|
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;
|
|
@@ -301,7 +388,7 @@ export function createAgentServer(
|
|
|
301
388
|
cors = true,
|
|
302
389
|
serverName = "agents-sdk",
|
|
303
390
|
serverVersion = "1.0.0",
|
|
304
|
-
secretStore
|
|
391
|
+
secretStore,
|
|
305
392
|
} = options;
|
|
306
393
|
|
|
307
394
|
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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(
|
|
579
|
+
const client = await authConfig.store.validateClient(
|
|
580
|
+
clientId,
|
|
581
|
+
clientSecret,
|
|
582
|
+
);
|
|
485
583
|
if (!client) {
|
|
486
584
|
return jsonResponse(
|
|
487
|
-
{
|
|
585
|
+
{
|
|
586
|
+
error: "invalid_client",
|
|
587
|
+
error_description: "Invalid client credentials",
|
|
588
|
+
},
|
|
488
589
|
401,
|
|
489
590
|
);
|
|
490
591
|
}
|
|
491
592
|
|
|
492
|
-
|
|
493
|
-
const
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
{
|
|
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(
|
|
608
|
-
console.log(
|
|
609
|
-
console.log(
|
|
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(
|
|
1143
|
+
console.log(" POST /oauth/token - OAuth2 token endpoint");
|
|
612
1144
|
console.log(" Auth: enabled");
|
|
613
1145
|
}
|
|
614
|
-
console.log(
|
|
1146
|
+
console.log(" MCP tools: call_agent, list_agents");
|
|
615
1147
|
},
|
|
616
1148
|
|
|
617
1149
|
async stop(): Promise<void> {
|