@slashfi/agents-sdk 0.5.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/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +16 -3
- package/dist/agent-definitions/auth.js.map +1 -1
- 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.map +1 -1
- package/dist/agent-definitions/secrets.js +8 -25
- package/dist/agent-definitions/secrets.js.map +1 -1
- 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.map +1 -1
- package/dist/crypto.js.map +1 -1
- 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 +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- 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 +477 -26
- 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/agent-definitions/auth.ts +37 -14
- package/src/agent-definitions/integrations.ts +1209 -0
- package/src/agent-definitions/secrets.ts +24 -30
- package/src/agent-definitions/users.ts +533 -0
- package/src/crypto.ts +3 -1
- package/src/define.ts +8 -0
- package/src/index.ts +56 -3
- package/src/jwt.ts +7 -5
- package/src/server.ts +565 -33
- package/src/slack-oauth.ts +66 -0
- package/src/types.ts +83 -0
- package/src/web-pages.ts +178 -0
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 {
|
|
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, "\&").replace(/</g, "\<").replace(/>/g, "\>").replace(/"/g, "\"");
|
|
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 {
|
|
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 {
|
|
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 ??
|
|
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")
|
|
@@ -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({
|
|
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({
|
|
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({
|
|
383
|
+
return jsonResponse({
|
|
384
|
+
error: "invalid_client",
|
|
385
|
+
error_description: "Invalid client credentials",
|
|
386
|
+
}, 401);
|
|
325
387
|
}
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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:
|
|
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
|
-
|
|
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({
|
|
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(
|
|
425
|
-
console.log(
|
|
426
|
-
console.log(
|
|
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(
|
|
879
|
+
console.log(" POST /oauth/token - OAuth2 token endpoint");
|
|
429
880
|
console.log(" Auth: enabled");
|
|
430
881
|
}
|
|
431
|
-
console.log(
|
|
882
|
+
console.log(" MCP tools: call_agent, list_agents");
|
|
432
883
|
},
|
|
433
884
|
async stop() {
|
|
434
885
|
if (serverInstance) {
|