@slashfi/agents-sdk 0.8.0 → 0.9.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 +17 -0
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +135 -1
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +19 -0
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +218 -5
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/integration-interface.d.ts +37 -0
- package/dist/integration-interface.d.ts.map +1 -0
- package/dist/integration-interface.js +94 -0
- package/dist/integration-interface.js.map +1 -0
- package/dist/integrations-store.d.ts +33 -0
- package/dist/integrations-store.d.ts.map +1 -0
- package/dist/integrations-store.js +50 -0
- package/dist/integrations-store.js.map +1 -0
- package/dist/jwt.d.ts +86 -17
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +140 -17
- package/dist/jwt.js.map +1 -1
- package/dist/registry.d.ts +7 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -21
- package/dist/registry.js.map +1 -1
- package/dist/secret-collection.d.ts +37 -0
- package/dist/secret-collection.d.ts.map +1 -0
- package/dist/secret-collection.js +37 -0
- package/dist/secret-collection.js.map +1 -0
- package/dist/server.d.ts +41 -44
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +232 -592
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/agent-definitions/auth.ts +187 -1
- package/src/agent-definitions/integrations.ts +260 -5
- package/src/index.ts +18 -4
- package/src/integration-interface.ts +118 -0
- package/src/integrations-store.ts +84 -0
- package/src/jwt.ts +233 -65
- package/src/registry.ts +17 -2
- package/src/secret-collection.ts +66 -0
- package/src/server.ts +268 -681
- package/src/types.ts +8 -1
- package/dist/slack-oauth.d.ts +0 -27
- package/dist/slack-oauth.d.ts.map +0 -1
- package/dist/slack-oauth.js +0 -48
- package/dist/slack-oauth.js.map +0 -1
- package/dist/web-pages.d.ts +0 -8
- package/dist/web-pages.d.ts.map +0 -1
- package/dist/web-pages.js +0 -169
- package/dist/web-pages.js.map +0 -1
- package/src/slack-oauth.ts +0 -66
- package/src/web-pages.ts +0 -178
package/dist/server.js
CHANGED
|
@@ -1,75 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent Server (MCP over HTTP)
|
|
3
3
|
*
|
|
4
|
-
* JSON-RPC server implementing the MCP protocol for agent interaction.
|
|
5
|
-
*
|
|
4
|
+
* Minimal JSON-RPC server implementing the MCP protocol for agent interaction.
|
|
5
|
+
* Handles only core SDK concerns:
|
|
6
|
+
* - MCP protocol (initialize, tools/list, tools/call)
|
|
7
|
+
* - Agent registry routing (call_agent, list_agents)
|
|
8
|
+
* - Auth resolution (Bearer tokens, root key, JWT)
|
|
9
|
+
* - OAuth2 token exchange (client_credentials)
|
|
10
|
+
* - Health check
|
|
11
|
+
* - CORS
|
|
6
12
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - tools/list → List available MCP tools (call_agent, list_agents)
|
|
10
|
-
* - tools/call → Execute an MCP tool
|
|
13
|
+
* Application-specific routes (web UI, OAuth callbacks, tenant management)
|
|
14
|
+
* should be built on top using the exported `fetch` handler.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Standalone usage
|
|
19
|
+
* const server = createAgentServer(registry, { port: 3000 });
|
|
20
|
+
* await server.start();
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - GET /health → Health check
|
|
21
|
-
*
|
|
22
|
-
* Auth Integration:
|
|
23
|
-
* When an `@auth` agent is registered, the server automatically:
|
|
24
|
-
* - Validates Bearer tokens on requests
|
|
25
|
-
* - Resolves tokens to identity + scopes
|
|
26
|
-
* - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
|
|
27
|
-
* - Recognizes the root key for admin access
|
|
22
|
+
* // Composable with any HTTP framework
|
|
23
|
+
* const server = createAgentServer(registry);
|
|
24
|
+
* app.all('/mcp/*', (req) => server.fetch(req));
|
|
25
|
+
* ```
|
|
28
26
|
*/
|
|
29
27
|
import { processSecretParams, } from "./agent-definitions/secrets.js";
|
|
30
28
|
import { verifyJwt } from "./jwt.js";
|
|
31
|
-
import {
|
|
32
|
-
import { slackAuthUrl, exchangeSlackCode, getSlackProfile } from "./slack-oauth.js";
|
|
33
|
-
function resolveBaseUrl(req, url) {
|
|
34
|
-
const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
|
35
|
-
const host = req.headers.get("x-forwarded-host") || url.host;
|
|
36
|
-
return `${proto}://${host}`;
|
|
37
|
-
}
|
|
29
|
+
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
|
|
38
30
|
// ============================================
|
|
39
|
-
//
|
|
40
|
-
// ============================================
|
|
41
|
-
function escHtml(s) {
|
|
42
|
-
return s.replace(/&/g, "\&").replace(/</g, "\<").replace(/>/g, "\>").replace(/"/g, "\"");
|
|
43
|
-
}
|
|
44
|
-
function renderSecretForm(token, pending, baseUrl) {
|
|
45
|
-
const fields = pending.fields.map(f => `
|
|
46
|
-
<div class="field">
|
|
47
|
-
<label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
|
|
48
|
-
${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
|
|
49
|
-
<input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
|
|
50
|
-
</div>`).join("");
|
|
51
|
-
return `<!DOCTYPE html>
|
|
52
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
|
|
53
|
-
<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>
|
|
54
|
-
<div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
|
|
55
|
-
<p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
|
|
56
|
-
<div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
|
|
57
|
-
<form id="f">${fields}<button type="submit">Submit Securely</button></form>
|
|
58
|
-
<p class="footer">Expires in 10 minutes</p></div>
|
|
59
|
-
<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>
|
|
60
|
-
<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>`;
|
|
61
|
-
}
|
|
62
|
-
export const pendingCollections = new Map();
|
|
63
|
-
export function generateCollectionToken() {
|
|
64
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
65
|
-
let token = "sc_";
|
|
66
|
-
for (let i = 0; i < 32; i++) {
|
|
67
|
-
token += chars[Math.floor(Math.random() * chars.length)];
|
|
68
|
-
}
|
|
69
|
-
return token;
|
|
70
|
-
}
|
|
71
|
-
// ============================================
|
|
72
|
-
// Helpers
|
|
31
|
+
// HTTP Helpers
|
|
73
32
|
// ============================================
|
|
74
33
|
function jsonResponse(data, status = 200) {
|
|
75
34
|
return new Response(JSON.stringify(data), {
|
|
@@ -81,9 +40,19 @@ function corsHeaders() {
|
|
|
81
40
|
return {
|
|
82
41
|
"Access-Control-Allow-Origin": "*",
|
|
83
42
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
84
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-
|
|
43
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
|
|
85
44
|
};
|
|
86
45
|
}
|
|
46
|
+
function addCors(res) {
|
|
47
|
+
const headers = new Headers(res.headers);
|
|
48
|
+
for (const [k, v] of Object.entries(corsHeaders())) {
|
|
49
|
+
headers.set(k, v);
|
|
50
|
+
}
|
|
51
|
+
return new Response(res.body, { status: res.status, headers });
|
|
52
|
+
}
|
|
53
|
+
// ============================================
|
|
54
|
+
// JSON-RPC Helpers
|
|
55
|
+
// ============================================
|
|
87
56
|
function jsonRpcSuccess(id, result) {
|
|
88
57
|
return { jsonrpc: "2.0", id, result };
|
|
89
58
|
}
|
|
@@ -107,9 +76,9 @@ function mcpResult(value, isError = false) {
|
|
|
107
76
|
};
|
|
108
77
|
}
|
|
109
78
|
// ============================================
|
|
110
|
-
// Auth Detection
|
|
79
|
+
// Auth Detection & Resolution
|
|
111
80
|
// ============================================
|
|
112
|
-
function detectAuth(registry) {
|
|
81
|
+
export function detectAuth(registry) {
|
|
113
82
|
const authAgent = registry.get("@auth");
|
|
114
83
|
if (!authAgent?.__authStore || !authAgent.__rootKey)
|
|
115
84
|
return null;
|
|
@@ -119,13 +88,14 @@ function detectAuth(registry) {
|
|
|
119
88
|
tokenTtl: authAgent.__tokenTtl ?? 3600,
|
|
120
89
|
};
|
|
121
90
|
}
|
|
122
|
-
async function resolveAuth(req, authConfig) {
|
|
91
|
+
export async function resolveAuth(req, authConfig, jwksOptions) {
|
|
123
92
|
const authHeader = req.headers.get("Authorization");
|
|
124
93
|
if (!authHeader)
|
|
125
94
|
return null;
|
|
126
95
|
const [scheme, credential] = authHeader.split(" ", 2);
|
|
127
96
|
if (scheme?.toLowerCase() !== "bearer" || !credential)
|
|
128
97
|
return null;
|
|
98
|
+
// Root key check
|
|
129
99
|
if (credential === authConfig.rootKey) {
|
|
130
100
|
return {
|
|
131
101
|
callerId: "root",
|
|
@@ -134,18 +104,52 @@ async function resolveAuth(req, authConfig) {
|
|
|
134
104
|
isRoot: true,
|
|
135
105
|
};
|
|
136
106
|
}
|
|
137
|
-
// Try
|
|
138
|
-
// JWT is signed with the client's secret hash
|
|
139
|
-
// Decode payload to get client_id, look up client, verify signature
|
|
107
|
+
// Try ES256 verification against own signing keys
|
|
140
108
|
const parts = credential.split(".");
|
|
109
|
+
if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
|
|
110
|
+
for (const key of jwksOptions.signingKeys) {
|
|
111
|
+
try {
|
|
112
|
+
const verified = await verifyJwtLocal(credential, key.publicKey);
|
|
113
|
+
if (verified) {
|
|
114
|
+
return {
|
|
115
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
116
|
+
callerType: "agent",
|
|
117
|
+
scopes: verified.scopes ?? ["*"],
|
|
118
|
+
isRoot: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Try trusted issuers (remote JWKS verification)
|
|
128
|
+
if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
|
|
129
|
+
for (const issuer of jwksOptions.trustedIssuers) {
|
|
130
|
+
try {
|
|
131
|
+
const verified = await verifyJwtFromIssuer(credential, issuer);
|
|
132
|
+
if (verified) {
|
|
133
|
+
return {
|
|
134
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
135
|
+
callerType: "agent",
|
|
136
|
+
scopes: verified.scopes ?? ["*"],
|
|
137
|
+
isRoot: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Try HMAC JWT verification (legacy, stateless)
|
|
141
147
|
if (parts.length === 3) {
|
|
142
|
-
// Looks like a JWT - decode payload to get client_id
|
|
143
148
|
try {
|
|
144
149
|
const payloadB64 = parts[1];
|
|
145
150
|
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
146
151
|
const payload = JSON.parse(atob(padded));
|
|
147
152
|
if (payload.sub) {
|
|
148
|
-
// Look up client to get the signing secret (secret hash)
|
|
149
153
|
const client = await authConfig.store.getClient(payload.sub);
|
|
150
154
|
if (client) {
|
|
151
155
|
const verified = await verifyJwt(credential, client.clientSecretHash);
|
|
@@ -176,7 +180,7 @@ async function resolveAuth(req, authConfig) {
|
|
|
176
180
|
isRoot: false,
|
|
177
181
|
};
|
|
178
182
|
}
|
|
179
|
-
function canSeeAgent(agent, auth) {
|
|
183
|
+
export function canSeeAgent(agent, auth) {
|
|
180
184
|
const visibility = (agent.visibility ??
|
|
181
185
|
agent.config?.visibility ??
|
|
182
186
|
"internal");
|
|
@@ -210,11 +214,11 @@ function getToolDefinitions() {
|
|
|
210
214
|
},
|
|
211
215
|
path: {
|
|
212
216
|
type: "string",
|
|
213
|
-
description: "Agent path (e.g
|
|
217
|
+
description: "Agent path (e.g., '@my-agent')",
|
|
214
218
|
},
|
|
215
219
|
tool: {
|
|
216
220
|
type: "string",
|
|
217
|
-
description: "Tool name to call
|
|
221
|
+
description: "Tool name to call",
|
|
218
222
|
},
|
|
219
223
|
params: {
|
|
220
224
|
type: "object",
|
|
@@ -243,29 +247,28 @@ function getToolDefinitions() {
|
|
|
243
247
|
// ============================================
|
|
244
248
|
export function createAgentServer(registry, options = {}) {
|
|
245
249
|
const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore, } = options;
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
// Signing keys for JWKS-based auth
|
|
251
|
+
const serverSigningKeys = [];
|
|
252
|
+
const configTrustedIssuers = options.trustedIssuers ?? [];
|
|
248
253
|
const authConfig = detectAuth(registry);
|
|
254
|
+
let serverInstance = null;
|
|
249
255
|
// ──────────────────────────────────────────
|
|
250
|
-
//
|
|
256
|
+
// JSON-RPC handler
|
|
251
257
|
// ──────────────────────────────────────────
|
|
252
258
|
async function handleJsonRpc(request, auth) {
|
|
253
259
|
switch (request.method) {
|
|
254
|
-
// MCP protocol handshake
|
|
255
260
|
case "initialize":
|
|
256
261
|
return jsonRpcSuccess(request.id, {
|
|
257
262
|
protocolVersion: "2024-11-05",
|
|
258
|
-
capabilities: { tools: {} },
|
|
263
|
+
capabilities: { tools: { listChanged: false } },
|
|
259
264
|
serverInfo: { name: serverName, version: serverVersion },
|
|
260
265
|
});
|
|
261
266
|
case "notifications/initialized":
|
|
262
267
|
return jsonRpcSuccess(request.id, {});
|
|
263
|
-
// List MCP tools
|
|
264
268
|
case "tools/list":
|
|
265
269
|
return jsonRpcSuccess(request.id, {
|
|
266
270
|
tools: getToolDefinitions(),
|
|
267
271
|
});
|
|
268
|
-
// Call an MCP tool
|
|
269
272
|
case "tools/call": {
|
|
270
273
|
const { name, arguments: args } = (request.params ?? {});
|
|
271
274
|
try {
|
|
@@ -273,7 +276,7 @@ export function createAgentServer(registry, options = {}) {
|
|
|
273
276
|
return jsonRpcSuccess(request.id, result);
|
|
274
277
|
}
|
|
275
278
|
catch (err) {
|
|
276
|
-
console.error("[server]
|
|
279
|
+
console.error("[server] Tool call error:", err);
|
|
277
280
|
return jsonRpcSuccess(request.id, mcpResult(`Error: ${err instanceof Error ? err.message : String(err)}`, true));
|
|
278
281
|
}
|
|
279
282
|
}
|
|
@@ -301,10 +304,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
301
304
|
req.callerType = "system";
|
|
302
305
|
}
|
|
303
306
|
// Process secret params: resolve refs, store raw secrets
|
|
304
|
-
// Auto-resolve secret:xxx refs in tool params before execution
|
|
305
307
|
if (req.params && secretStore) {
|
|
306
308
|
const ownerId = auth?.callerId ?? "anonymous";
|
|
307
|
-
// Find the tool schema to check for secret: true fields
|
|
308
309
|
const agent = registry.get(req.path);
|
|
309
310
|
const tool = agent?.tools.find((t) => t.name === req.tool);
|
|
310
311
|
const schema = tool?.inputSchema;
|
|
@@ -332,6 +333,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
332
333
|
return true;
|
|
333
334
|
if (tv === "public")
|
|
334
335
|
return true;
|
|
336
|
+
if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous")
|
|
337
|
+
return true;
|
|
335
338
|
if (tv === "internal" && auth)
|
|
336
339
|
return true;
|
|
337
340
|
return false;
|
|
@@ -345,7 +348,7 @@ export function createAgentServer(registry, options = {}) {
|
|
|
345
348
|
}
|
|
346
349
|
}
|
|
347
350
|
// ──────────────────────────────────────────
|
|
348
|
-
// OAuth2 token handler
|
|
351
|
+
// OAuth2 token handler
|
|
349
352
|
// ──────────────────────────────────────────
|
|
350
353
|
async function handleOAuthToken(req) {
|
|
351
354
|
if (!authConfig) {
|
|
@@ -380,556 +383,193 @@ export function createAgentServer(registry, options = {}) {
|
|
|
380
383
|
error_description: "Missing client_id or client_secret",
|
|
381
384
|
}, 400);
|
|
382
385
|
}
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
try {
|
|
387
|
+
const result = await registry.call({
|
|
388
|
+
action: "execute_tool",
|
|
389
|
+
path: "@auth",
|
|
390
|
+
tool: "token",
|
|
391
|
+
params: { clientId, clientSecret },
|
|
392
|
+
callerType: "system",
|
|
393
|
+
});
|
|
394
|
+
const tokenResult = result?.result;
|
|
395
|
+
if (!tokenResult?.accessToken) {
|
|
396
|
+
return jsonResponse({ error: "invalid_client", error_description: "Authentication failed" }, 401);
|
|
397
|
+
}
|
|
385
398
|
return jsonResponse({
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
399
|
+
access_token: tokenResult.accessToken,
|
|
400
|
+
token_type: "Bearer",
|
|
401
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
402
|
+
refresh_token: tokenResult.refreshToken,
|
|
403
|
+
});
|
|
389
404
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
path: "@auth",
|
|
394
|
-
tool: "token",
|
|
395
|
-
params: {
|
|
396
|
-
grantType: "client_credentials",
|
|
397
|
-
clientId,
|
|
398
|
-
clientSecret,
|
|
399
|
-
},
|
|
400
|
-
context: {
|
|
401
|
-
tenantId: "default",
|
|
402
|
-
agentPath: "@auth",
|
|
403
|
-
callerId: "oauth_endpoint",
|
|
404
|
-
callerType: "system",
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
// Extract the result - registry.call returns { success, result: { accessToken, tokenType, expiresIn, scopes } }
|
|
408
|
-
const callResponse = tokenResult;
|
|
409
|
-
if (!callResponse.success) {
|
|
410
|
-
return jsonResponse({ error: "token_generation_failed", error_description: callResponse.error ?? "Unknown error" }, 500);
|
|
405
|
+
catch (err) {
|
|
406
|
+
console.error("[oauth] Token error:", err);
|
|
407
|
+
return jsonResponse({ error: "server_error", error_description: "Token exchange failed" }, 500);
|
|
411
408
|
}
|
|
412
|
-
const tokenData = callResponse.result;
|
|
413
|
-
// accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
|
|
414
|
-
const accessToken = tokenData.accessToken?.$agent_type === "secret"
|
|
415
|
-
? tokenData.accessToken.value
|
|
416
|
-
: tokenData.accessToken;
|
|
417
|
-
return jsonResponse({
|
|
418
|
-
access_token: accessToken,
|
|
419
|
-
token_type: tokenData.tokenType ?? "Bearer",
|
|
420
|
-
expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
|
|
421
|
-
scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
|
|
422
|
-
});
|
|
423
409
|
}
|
|
424
410
|
// ──────────────────────────────────────────
|
|
425
|
-
//
|
|
426
|
-
//
|
|
411
|
+
// Main fetch handler
|
|
412
|
+
// ──────────────────────────────────────────
|
|
427
413
|
async function fetch(req) {
|
|
428
|
-
const url = new URL(req.url);
|
|
429
|
-
const path = url.pathname.replace(basePath, "") || "/";
|
|
430
|
-
// CORS preflight
|
|
431
|
-
if (cors && req.method === "OPTIONS") {
|
|
432
|
-
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
433
|
-
}
|
|
434
|
-
const addCors = (response) => {
|
|
435
|
-
if (!cors)
|
|
436
|
-
return response;
|
|
437
|
-
const headers = new Headers(response.headers);
|
|
438
|
-
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
439
|
-
headers.set(key, value);
|
|
440
|
-
}
|
|
441
|
-
return new Response(response.body, {
|
|
442
|
-
status: response.status,
|
|
443
|
-
statusText: response.statusText,
|
|
444
|
-
headers,
|
|
445
|
-
});
|
|
446
|
-
};
|
|
447
|
-
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
448
414
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return
|
|
415
|
+
const url = new URL(req.url);
|
|
416
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
417
|
+
// CORS preflight
|
|
418
|
+
if (cors && req.method === "OPTIONS") {
|
|
419
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
454
420
|
}
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
description: agent.config?.description,
|
|
473
|
-
supportedActions: agent.config?.supportedActions,
|
|
474
|
-
integration: agent.config?.integration || null,
|
|
475
|
-
tools: agent.tools
|
|
476
|
-
.filter((t) => {
|
|
477
|
-
const tv = t.visibility ?? "internal";
|
|
478
|
-
if (auth?.isRoot)
|
|
479
|
-
return true;
|
|
480
|
-
if (tv === "public")
|
|
481
|
-
return true;
|
|
482
|
-
if (tv === "internal" && auth)
|
|
483
|
-
return true;
|
|
484
|
-
return false;
|
|
485
|
-
})
|
|
486
|
-
.map((t) => t.name),
|
|
487
|
-
})),
|
|
488
|
-
}));
|
|
489
|
-
}
|
|
490
|
-
// ---- Shared OAuth callback handler ----
|
|
491
|
-
async function handleIntegrationOAuthCallback(provider, req) {
|
|
492
|
-
const url = new URL(req.url);
|
|
493
|
-
const code = url.searchParams.get("code");
|
|
494
|
-
const state = url.searchParams.get("state");
|
|
495
|
-
const oauthError = url.searchParams.get("error");
|
|
496
|
-
const errorDescription = url.searchParams.get("error_description");
|
|
497
|
-
if (oauthError) {
|
|
498
|
-
return new Response(`<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`, { status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } });
|
|
499
|
-
}
|
|
500
|
-
if (!code) {
|
|
501
|
-
return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
|
|
502
|
-
}
|
|
503
|
-
try {
|
|
504
|
-
await registry.call({
|
|
505
|
-
action: "execute_tool",
|
|
506
|
-
path: "@integrations",
|
|
507
|
-
tool: "handle_oauth_callback",
|
|
508
|
-
params: { provider, code, state: state ?? undefined },
|
|
509
|
-
context: {
|
|
510
|
-
tenantId: "default",
|
|
511
|
-
agentPath: "@integrations",
|
|
512
|
-
callerId: "oauth_callback",
|
|
513
|
-
callerType: "system",
|
|
514
|
-
},
|
|
515
|
-
});
|
|
516
|
-
// Parse redirect URL from state (base64-encoded JSON)
|
|
517
|
-
let redirectUrl = "/";
|
|
518
|
-
if (state) {
|
|
519
|
-
try {
|
|
520
|
-
const parsed = JSON.parse(atob(state));
|
|
521
|
-
if (parsed.redirectUrl)
|
|
522
|
-
redirectUrl = parsed.redirectUrl;
|
|
523
|
-
}
|
|
524
|
-
catch {
|
|
525
|
-
// Fallback: try raw JSON for backward compat
|
|
526
|
-
try {
|
|
527
|
-
const parsed = JSON.parse(state);
|
|
528
|
-
if (parsed.redirectUrl)
|
|
529
|
-
redirectUrl = parsed.redirectUrl;
|
|
530
|
-
}
|
|
531
|
-
catch { }
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
const sep = redirectUrl.includes("?") ? "&" : "?";
|
|
535
|
-
return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
|
|
536
|
-
}
|
|
537
|
-
catch (err) {
|
|
538
|
-
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() } });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
// GET /oauth/callback - Unified OAuth callback (provider from state param)
|
|
542
|
-
if (path === "/oauth/callback" && req.method === "GET") {
|
|
543
|
-
const url = new URL(req.url);
|
|
544
|
-
const state = url.searchParams.get("state");
|
|
545
|
-
let provider;
|
|
546
|
-
if (state) {
|
|
547
|
-
try {
|
|
548
|
-
const parsed = JSON.parse(atob(state));
|
|
549
|
-
provider = parsed.providerId;
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
// Fallback: try raw JSON for backward compat
|
|
553
|
-
try {
|
|
554
|
-
const parsed = JSON.parse(state);
|
|
555
|
-
provider = parsed.providerId;
|
|
556
|
-
}
|
|
557
|
-
catch { }
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
if (!provider) {
|
|
561
|
-
return addCors(jsonResponse({ error: "Missing provider in state param" }, 400));
|
|
562
|
-
}
|
|
563
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
564
|
-
}
|
|
565
|
-
// GET /integrations/callback/:provider - Legacy OAuth callback (provider from URL path)
|
|
566
|
-
if (path.startsWith("/integrations/callback/") && req.method === "GET") {
|
|
567
|
-
const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
|
|
568
|
-
if (!provider) {
|
|
569
|
-
return addCors(jsonResponse({ error: "Missing provider" }, 400));
|
|
570
|
-
}
|
|
571
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
572
|
-
}
|
|
573
|
-
// GET /secrets/form/:token - Serve hosted secrets form
|
|
574
|
-
if (path.startsWith("/secrets/form/") && req.method === "GET") {
|
|
575
|
-
const token = path.split("/").pop() ?? "";
|
|
576
|
-
const pending = pendingCollections.get(token);
|
|
577
|
-
if (!pending) {
|
|
578
|
-
return addCors(new Response("Invalid or expired form link", { status: 404 }));
|
|
579
|
-
}
|
|
580
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
581
|
-
pendingCollections.delete(token);
|
|
582
|
-
return addCors(new Response("Form link expired", { status: 410 }));
|
|
583
|
-
}
|
|
584
|
-
const reqUrl = new URL(req.url);
|
|
585
|
-
const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
586
|
-
const html = renderSecretForm(token, pending, baseUrl);
|
|
587
|
-
return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
|
|
588
|
-
}
|
|
589
|
-
// POST /secrets/collect - Submit collected secrets and auto-forward to tool
|
|
590
|
-
if (path === "/secrets/collect" && req.method === "POST") {
|
|
591
|
-
const body = (await req.json());
|
|
592
|
-
const pending = pendingCollections.get(body.token);
|
|
593
|
-
if (!pending) {
|
|
594
|
-
return addCors(jsonResponse({ error: "Invalid or expired collection token" }, 400));
|
|
595
|
-
}
|
|
596
|
-
// One-time use
|
|
597
|
-
pendingCollections.delete(body.token);
|
|
598
|
-
// Check expiry (10 min)
|
|
599
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
600
|
-
return addCors(jsonResponse({ error: "Collection token expired" }, 400));
|
|
601
|
-
}
|
|
602
|
-
// Encrypt secret values and store as refs
|
|
603
|
-
const mergedParams = { ...pending.params };
|
|
604
|
-
for (const [fieldName, value] of Object.entries(body.values)) {
|
|
605
|
-
const fieldDef = pending.fields.find((f) => f.name === fieldName);
|
|
606
|
-
if (fieldDef?.secret && secretStore) {
|
|
607
|
-
// Store encrypted, get ref
|
|
608
|
-
const ownerId = pending.auth?.callerId ?? "anonymous";
|
|
609
|
-
const secretId = await secretStore.store(value, ownerId);
|
|
610
|
-
mergedParams[fieldName] = `secret:${secretId}`;
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
mergedParams[fieldName] = value;
|
|
421
|
+
// Resolve auth for all requests
|
|
422
|
+
const auth = authConfig ? await resolveAuth(req, authConfig, {
|
|
423
|
+
signingKeys: serverSigningKeys,
|
|
424
|
+
trustedIssuers: configTrustedIssuers,
|
|
425
|
+
}) : null;
|
|
426
|
+
// Also check header-based identity (for proxied requests)
|
|
427
|
+
const headerAuth = !auth
|
|
428
|
+
? (() => {
|
|
429
|
+
const actorId = req.headers.get("X-Atlas-Actor-Id");
|
|
430
|
+
const actorType = req.headers.get("X-Atlas-Actor-Type");
|
|
431
|
+
if (actorId) {
|
|
432
|
+
return {
|
|
433
|
+
callerId: actorId,
|
|
434
|
+
callerType: actorType ?? "agent",
|
|
435
|
+
scopes: ["*"],
|
|
436
|
+
isRoot: false,
|
|
437
|
+
};
|
|
614
438
|
}
|
|
615
|
-
}
|
|
616
|
-
// Auto-forward to the target tool
|
|
617
|
-
const callRequest = {
|
|
618
|
-
action: "execute_tool",
|
|
619
|
-
path: pending.agent,
|
|
620
|
-
tool: pending.tool,
|
|
621
|
-
params: mergedParams,
|
|
622
|
-
};
|
|
623
|
-
const toolCtx = {
|
|
624
|
-
tenantId: "default",
|
|
625
|
-
agentPath: pending.agent,
|
|
626
|
-
callerId: pending.auth?.callerId ?? "anonymous",
|
|
627
|
-
callerType: pending.auth?.callerType ?? "system",
|
|
628
|
-
};
|
|
629
|
-
const result = await registry.call({
|
|
630
|
-
...callRequest,
|
|
631
|
-
context: toolCtx,
|
|
632
|
-
});
|
|
633
|
-
return addCors(jsonResponse({ success: true, result }));
|
|
634
|
-
}
|
|
635
|
-
// --- Web pages (plain HTML, served from same server) ---
|
|
636
|
-
const htmlRes = (body) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
|
|
637
|
-
const reqUrl = new URL(req.url);
|
|
638
|
-
const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
639
|
-
const slackConfig = process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
|
|
640
|
-
? {
|
|
641
|
-
clientId: process.env.SLACK_CLIENT_ID,
|
|
642
|
-
clientSecret: process.env.SLACK_CLIENT_SECRET,
|
|
643
|
-
redirectUri: `${baseUrl}/auth/slack/callback`,
|
|
644
|
-
}
|
|
645
|
-
: null;
|
|
646
|
-
// Helper: read session from cookie
|
|
647
|
-
function getSession(r) {
|
|
648
|
-
const c = r.headers.get("Cookie") || "";
|
|
649
|
-
const m = c.match(/s_session=([^;]+)/);
|
|
650
|
-
if (!m)
|
|
651
|
-
return null;
|
|
652
|
-
try {
|
|
653
|
-
return JSON.parse(Buffer.from(m[1], "base64url").toString());
|
|
654
|
-
}
|
|
655
|
-
catch {
|
|
656
439
|
return null;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const cid = clientRes?.result?.clientId;
|
|
666
|
-
const csec = clientRes?.result?.clientSecret;
|
|
667
|
-
if (!cid || !csec)
|
|
668
|
-
throw new Error("Failed to create client: " + JSON.stringify(clientRes));
|
|
669
|
-
const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
|
|
670
|
-
method: "POST",
|
|
671
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
672
|
-
body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
|
|
673
|
-
});
|
|
674
|
-
const tokenData = await tokenRes.json();
|
|
675
|
-
if (!tokenData.access_token)
|
|
676
|
-
throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
|
|
677
|
-
return tokenData.access_token;
|
|
678
|
-
}
|
|
679
|
-
// Helper: set session cookie and redirect
|
|
680
|
-
function sessionRedirect(location, session) {
|
|
681
|
-
const data = Buffer.from(JSON.stringify(session)).toString("base64url");
|
|
682
|
-
return new Response(null, {
|
|
683
|
-
status: 302,
|
|
684
|
-
headers: {
|
|
685
|
-
Location: location,
|
|
686
|
-
"Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
// GET / — login page (or redirect to dashboard if session exists)
|
|
691
|
-
if (path === "/" && req.method === "GET") {
|
|
692
|
-
const session = getSession(req);
|
|
693
|
-
if (session?.token)
|
|
694
|
-
return Response.redirect(`${baseUrl}/dashboard`, 302);
|
|
695
|
-
return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
|
|
440
|
+
})()
|
|
441
|
+
: null;
|
|
442
|
+
const effectiveAuth = auth ?? headerAuth;
|
|
443
|
+
// ── POST / → MCP JSON-RPC ──
|
|
444
|
+
if (path === "/" && req.method === "POST") {
|
|
445
|
+
const body = (await req.json());
|
|
446
|
+
const result = await handleJsonRpc(body, effectiveAuth);
|
|
447
|
+
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
696
448
|
}
|
|
697
|
-
//
|
|
698
|
-
if (path === "/
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return Response.redirect(slackAuthUrl(slackConfig), 302);
|
|
449
|
+
// ── POST /oauth/token → OAuth2 client_credentials ──
|
|
450
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
451
|
+
const res = await handleOAuthToken(req);
|
|
452
|
+
return cors ? addCors(res) : res;
|
|
702
453
|
}
|
|
703
|
-
// GET /
|
|
704
|
-
if (path === "/
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const authCode = reqUrl.searchParams.get("code");
|
|
708
|
-
const authError = reqUrl.searchParams.get("error");
|
|
709
|
-
if (authError || !authCode)
|
|
710
|
-
return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
|
|
711
|
-
try {
|
|
712
|
-
const tokens = await exchangeSlackCode(authCode, slackConfig);
|
|
713
|
-
const profile = await getSlackProfile(tokens.access_token);
|
|
714
|
-
const teamId = profile["https://slack.com/team_id"] || "";
|
|
715
|
-
const teamName = profile["https://slack.com/team_name"] || "";
|
|
716
|
-
// Check if user already exists
|
|
717
|
-
console.log("[auth] Looking up slack user:", profile.sub, profile.email);
|
|
718
|
-
const existing = await registry.call({
|
|
719
|
-
action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
|
|
720
|
-
params: { provider: "slack", providerUserId: profile.sub },
|
|
721
|
-
});
|
|
722
|
-
console.log("[auth] resolve_identity:", JSON.stringify(existing));
|
|
723
|
-
if (existing?.result?.found && existing?.result?.user?.tenantId) {
|
|
724
|
-
// Returning user — generate token and go to dashboard
|
|
725
|
-
console.log("[auth] Returning user, generating token...");
|
|
726
|
-
const mcpToken = await generateMcpToken();
|
|
727
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
728
|
-
userId: existing.result.user.id,
|
|
729
|
-
tenantId: existing.result.user.tenantId,
|
|
730
|
-
email: existing.result.user.email,
|
|
731
|
-
name: existing.result.user.name,
|
|
732
|
-
token: mcpToken,
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
// Check if Slack team already has a tenant
|
|
736
|
-
if (teamId) {
|
|
737
|
-
console.log("[auth] Checking tenant_identities for team:", teamId);
|
|
738
|
-
try {
|
|
739
|
-
// Direct DB query via the auth store's underlying connection
|
|
740
|
-
// We'll use a simple fetch to our own MCP endpoint to call @db-connections
|
|
741
|
-
// Actually, simpler: just query the DB directly via the server's context
|
|
742
|
-
// For now, use registry.call to a custom tool or direct SQL
|
|
743
|
-
// Simplest: call @auth list_tenants and check metadata
|
|
744
|
-
// Even simpler: direct SQL via globalThis.fetch to ourselves
|
|
745
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
746
|
-
if (dbUrl) {
|
|
747
|
-
const { default: postgres } = await import("postgres");
|
|
748
|
-
const sql = postgres(dbUrl);
|
|
749
|
-
const rows = await sql `SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
|
|
750
|
-
await sql.end();
|
|
751
|
-
if (rows.length > 0) {
|
|
752
|
-
const existingTenantId = rows[0].tenant_id;
|
|
753
|
-
console.log("[auth] Found existing tenant for team:", existingTenantId);
|
|
754
|
-
// Create user on existing tenant
|
|
755
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
|
|
756
|
-
email: profile.email, name: profile.name, tenantId: existingTenantId,
|
|
757
|
-
} });
|
|
758
|
-
const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
759
|
-
console.log("[auth] Created user on existing tenant:", newUserId);
|
|
760
|
-
// Link identity
|
|
761
|
-
if (newUserId) {
|
|
762
|
-
await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
763
|
-
userId: newUserId, provider: "slack", providerUserId: profile.sub,
|
|
764
|
-
email: profile.email, name: profile.name,
|
|
765
|
-
metadata: { slackTeamId: teamId, slackTeamName: teamName },
|
|
766
|
-
} });
|
|
767
|
-
}
|
|
768
|
-
// Generate token and go to dashboard
|
|
769
|
-
const mcpToken = await generateMcpToken();
|
|
770
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
771
|
-
userId: newUserId, tenantId: existingTenantId,
|
|
772
|
-
email: profile.email, name: profile.name, token: mcpToken,
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
catch (e) {
|
|
778
|
-
console.error("[auth] tenant_identity lookup error:", e.message);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// New user — redirect to setup
|
|
782
|
-
return sessionRedirect(`${baseUrl}/setup`, {
|
|
783
|
-
email: profile.email,
|
|
784
|
-
name: profile.name,
|
|
785
|
-
picture: profile.picture,
|
|
786
|
-
slackUserId: profile.sub,
|
|
787
|
-
slackTeamId: teamId,
|
|
788
|
-
slackTeamName: teamName,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
catch (err) {
|
|
792
|
-
console.error("[auth] callback error:", err);
|
|
793
|
-
return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
|
|
794
|
-
}
|
|
454
|
+
// ── GET /health → Health check ──
|
|
455
|
+
if (path === "/health" && req.method === "GET") {
|
|
456
|
+
const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
|
|
457
|
+
return cors ? addCors(res) : res;
|
|
795
458
|
}
|
|
796
|
-
// GET /
|
|
797
|
-
if (path === "/
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
459
|
+
// ── GET /.well-known/jwks.json → JWKS public keys ──
|
|
460
|
+
if (path === "/.well-known/jwks.json" && req.method === "GET") {
|
|
461
|
+
const jwks = serverSigningKeys.length > 0
|
|
462
|
+
? await buildJwks(serverSigningKeys)
|
|
463
|
+
: { keys: [] };
|
|
464
|
+
const res = jsonResponse(jwks);
|
|
465
|
+
return cors ? addCors(res) : res;
|
|
802
466
|
}
|
|
803
|
-
//
|
|
804
|
-
if (path === "/
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
console.log("[setup] tenant created:", tenantId);
|
|
815
|
-
// 2. Create user
|
|
816
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } });
|
|
817
|
-
const userId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
818
|
-
console.log("[setup] user created:", userId);
|
|
819
|
-
// 2b. Link tenant to Slack team
|
|
820
|
-
if (session?.slackTeamId) {
|
|
821
|
-
try {
|
|
822
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
823
|
-
if (dbUrl) {
|
|
824
|
-
const { default: postgres } = await import("postgres");
|
|
825
|
-
const sql = postgres(dbUrl);
|
|
826
|
-
const id = "ti_" + Math.random().toString(36).slice(2, 14);
|
|
827
|
-
await sql `INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
|
|
828
|
-
await sql.end();
|
|
829
|
-
console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
catch (e) {
|
|
833
|
-
console.error("[setup] tenant_identity insert error:", e.message);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
// 3. Link Slack identity
|
|
837
|
-
if (session?.slackUserId && userId) {
|
|
838
|
-
console.log("[setup] linking slack identity:", session.slackUserId);
|
|
839
|
-
const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
840
|
-
userId,
|
|
841
|
-
provider: "slack",
|
|
842
|
-
providerUserId: session.slackUserId,
|
|
843
|
-
email: body.email,
|
|
844
|
-
name: session.name,
|
|
845
|
-
metadata: { slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system"
|
|
846
|
-
} });
|
|
847
|
-
console.log("[setup] link_identity result:", JSON.stringify(linkRes));
|
|
848
|
-
}
|
|
849
|
-
// 4. Generate MCP token
|
|
850
|
-
const mcpToken = await generateMcpToken();
|
|
851
|
-
console.log("[setup] token generated, length:", mcpToken.length);
|
|
852
|
-
return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
|
|
853
|
-
}
|
|
854
|
-
catch (err) {
|
|
855
|
-
console.error("[setup] error:", err);
|
|
856
|
-
return addCors(jsonResponse({ error: err.message }, 400));
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
// POST /logout — clear session
|
|
860
|
-
if (path === "/logout" && req.method === "POST") {
|
|
861
|
-
return new Response(null, {
|
|
862
|
-
status: 302,
|
|
863
|
-
headers: {
|
|
864
|
-
Location: `${baseUrl}/`,
|
|
865
|
-
"Set-Cookie": "s_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
|
|
866
|
-
},
|
|
467
|
+
// ── GET /.well-known/configuration → Server discovery ──
|
|
468
|
+
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
469
|
+
const baseUrl = new URL(req.url).origin;
|
|
470
|
+
const res = jsonResponse({
|
|
471
|
+
issuer: baseUrl,
|
|
472
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
473
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
474
|
+
agents_endpoint: `${baseUrl}/list`,
|
|
475
|
+
call_endpoint: baseUrl,
|
|
476
|
+
supported_grant_types: ["client_credentials"],
|
|
477
|
+
agents: registry.listPaths(),
|
|
867
478
|
});
|
|
479
|
+
return cors ? addCors(res) : res;
|
|
868
480
|
}
|
|
869
|
-
// GET /
|
|
870
|
-
if (path === "/
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
481
|
+
// ── GET /list → List agents (legacy endpoint) ──
|
|
482
|
+
if (path === "/list" && req.method === "GET") {
|
|
483
|
+
const agents = registry.list();
|
|
484
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
|
|
485
|
+
const res = jsonResponse(visible.map((agent) => ({
|
|
486
|
+
path: agent.path,
|
|
487
|
+
name: agent.config?.name,
|
|
488
|
+
description: agent.config?.description,
|
|
489
|
+
supportedActions: agent.config?.supportedActions,
|
|
490
|
+
integration: agent.config?.integration || null,
|
|
491
|
+
tools: agent.tools
|
|
492
|
+
.filter((t) => {
|
|
493
|
+
const tv = t.visibility ?? "internal";
|
|
494
|
+
if (effectiveAuth?.isRoot)
|
|
495
|
+
return true;
|
|
496
|
+
if (tv === "public")
|
|
497
|
+
return true;
|
|
498
|
+
if (tv === "internal" && effectiveAuth)
|
|
499
|
+
return true;
|
|
500
|
+
return false;
|
|
501
|
+
})
|
|
502
|
+
.map((t) => ({
|
|
503
|
+
name: t.name,
|
|
504
|
+
description: t.description,
|
|
505
|
+
})),
|
|
506
|
+
})));
|
|
507
|
+
return cors ? addCors(res) : res;
|
|
883
508
|
}
|
|
884
|
-
|
|
509
|
+
// ── Not found ──
|
|
510
|
+
const res = jsonResponse({
|
|
885
511
|
jsonrpc: "2.0",
|
|
886
512
|
id: null,
|
|
887
513
|
error: {
|
|
888
514
|
code: -32601,
|
|
889
515
|
message: `Not found: ${req.method} ${path}`,
|
|
890
516
|
},
|
|
891
|
-
}, 404)
|
|
517
|
+
}, 404);
|
|
518
|
+
return cors ? addCors(res) : res;
|
|
892
519
|
}
|
|
893
520
|
catch (err) {
|
|
894
521
|
console.error("[server] Request error:", err);
|
|
895
|
-
|
|
522
|
+
const res = jsonResponse({
|
|
896
523
|
jsonrpc: "2.0",
|
|
897
524
|
id: null,
|
|
898
|
-
error: {
|
|
899
|
-
|
|
525
|
+
error: {
|
|
526
|
+
code: -32603,
|
|
527
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
528
|
+
},
|
|
529
|
+
}, 500);
|
|
530
|
+
return cors ? addCors(res) : res;
|
|
900
531
|
}
|
|
901
532
|
}
|
|
902
533
|
// ──────────────────────────────────────────
|
|
903
534
|
// Server lifecycle
|
|
904
535
|
// ──────────────────────────────────────────
|
|
905
|
-
|
|
536
|
+
return {
|
|
537
|
+
url: null,
|
|
538
|
+
registry,
|
|
906
539
|
async start() {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
console.log(" POST /oauth/token - OAuth2 token endpoint");
|
|
917
|
-
console.log(" Auth: enabled");
|
|
540
|
+
// Load or generate signing key for JWKS
|
|
541
|
+
if (options.signingKey) {
|
|
542
|
+
serverSigningKeys.push(options.signingKey);
|
|
543
|
+
}
|
|
544
|
+
else if (authConfig?.store?.getSigningKeys) {
|
|
545
|
+
const stored = await authConfig.store.getSigningKeys() ?? [];
|
|
546
|
+
for (const exported of stored) {
|
|
547
|
+
serverSigningKeys.push(await importSigningKey(exported));
|
|
548
|
+
}
|
|
918
549
|
}
|
|
919
|
-
|
|
550
|
+
if (serverSigningKeys.length === 0) {
|
|
551
|
+
const key = await generateSigningKey();
|
|
552
|
+
serverSigningKeys.push(key);
|
|
553
|
+
if (authConfig?.store?.storeSigningKey) {
|
|
554
|
+
await authConfig.store.storeSigningKey(await exportSigningKey(key));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
serverInstance = Bun.serve({
|
|
558
|
+
port,
|
|
559
|
+
hostname,
|
|
560
|
+
fetch,
|
|
561
|
+
});
|
|
562
|
+
this.url = `http://${hostname}:${port}`;
|
|
563
|
+
console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
|
|
920
564
|
},
|
|
921
565
|
async stop() {
|
|
922
566
|
if (serverInstance) {
|
|
923
567
|
serverInstance.stop();
|
|
924
568
|
serverInstance = null;
|
|
925
|
-
|
|
569
|
+
this.url = null;
|
|
926
570
|
}
|
|
927
571
|
},
|
|
928
572
|
fetch,
|
|
929
|
-
get url() {
|
|
930
|
-
return serverUrl;
|
|
931
|
-
},
|
|
932
573
|
};
|
|
933
|
-
return server;
|
|
934
574
|
}
|
|
935
575
|
//# sourceMappingURL=server.js.map
|