@slashfi/agents-sdk 0.7.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 +28 -12
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +239 -41
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts +32 -0
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -0
- package/dist/agent-definitions/remote-registry.js +460 -0
- package/dist/agent-definitions/remote-registry.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -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 +14 -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 -42
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +232 -555
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +24 -2
- 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 +287 -55
- package/src/agent-definitions/remote-registry.ts +621 -0
- package/src/index.ts +22 -5
- package/src/integration-interface.ts +118 -0
- package/src/integrations-store.ts +84 -0
- package/src/jwt.ts +233 -65
- package/src/registry.ts +23 -2
- package/src/secret-collection.ts +66 -0
- package/src/server.ts +268 -647
- package/src/types.ts +28 -2
- 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,73 +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
|
-
* Auth Integration:
|
|
21
|
-
* When an `@auth` agent is registered, the server automatically:
|
|
22
|
-
* - Validates Bearer tokens on requests
|
|
23
|
-
* - Resolves tokens to identity + scopes
|
|
24
|
-
* - Populates caller context from headers (X-Atlas-Actor-Id, etc.)
|
|
25
|
-
* - 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
|
+
* ```
|
|
26
26
|
*/
|
|
27
27
|
import { processSecretParams, } from "./agent-definitions/secrets.js";
|
|
28
28
|
import { verifyJwt } from "./jwt.js";
|
|
29
|
-
import {
|
|
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
|
-
}
|
|
29
|
+
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
|
|
36
30
|
// ============================================
|
|
37
|
-
//
|
|
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
|
-
}
|
|
69
|
-
// ============================================
|
|
70
|
-
// Helpers
|
|
31
|
+
// HTTP Helpers
|
|
71
32
|
// ============================================
|
|
72
33
|
function jsonResponse(data, status = 200) {
|
|
73
34
|
return new Response(JSON.stringify(data), {
|
|
@@ -79,9 +40,19 @@ function corsHeaders() {
|
|
|
79
40
|
return {
|
|
80
41
|
"Access-Control-Allow-Origin": "*",
|
|
81
42
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
82
|
-
"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",
|
|
83
44
|
};
|
|
84
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
|
+
// ============================================
|
|
85
56
|
function jsonRpcSuccess(id, result) {
|
|
86
57
|
return { jsonrpc: "2.0", id, result };
|
|
87
58
|
}
|
|
@@ -105,9 +76,9 @@ function mcpResult(value, isError = false) {
|
|
|
105
76
|
};
|
|
106
77
|
}
|
|
107
78
|
// ============================================
|
|
108
|
-
// Auth Detection
|
|
79
|
+
// Auth Detection & Resolution
|
|
109
80
|
// ============================================
|
|
110
|
-
function detectAuth(registry) {
|
|
81
|
+
export function detectAuth(registry) {
|
|
111
82
|
const authAgent = registry.get("@auth");
|
|
112
83
|
if (!authAgent?.__authStore || !authAgent.__rootKey)
|
|
113
84
|
return null;
|
|
@@ -117,13 +88,14 @@ function detectAuth(registry) {
|
|
|
117
88
|
tokenTtl: authAgent.__tokenTtl ?? 3600,
|
|
118
89
|
};
|
|
119
90
|
}
|
|
120
|
-
async function resolveAuth(req, authConfig) {
|
|
91
|
+
export async function resolveAuth(req, authConfig, jwksOptions) {
|
|
121
92
|
const authHeader = req.headers.get("Authorization");
|
|
122
93
|
if (!authHeader)
|
|
123
94
|
return null;
|
|
124
95
|
const [scheme, credential] = authHeader.split(" ", 2);
|
|
125
96
|
if (scheme?.toLowerCase() !== "bearer" || !credential)
|
|
126
97
|
return null;
|
|
98
|
+
// Root key check
|
|
127
99
|
if (credential === authConfig.rootKey) {
|
|
128
100
|
return {
|
|
129
101
|
callerId: "root",
|
|
@@ -132,18 +104,52 @@ async function resolveAuth(req, authConfig) {
|
|
|
132
104
|
isRoot: true,
|
|
133
105
|
};
|
|
134
106
|
}
|
|
135
|
-
// Try
|
|
136
|
-
// JWT is signed with the client's secret hash
|
|
137
|
-
// Decode payload to get client_id, look up client, verify signature
|
|
107
|
+
// Try ES256 verification against own signing keys
|
|
138
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)
|
|
139
147
|
if (parts.length === 3) {
|
|
140
|
-
// Looks like a JWT - decode payload to get client_id
|
|
141
148
|
try {
|
|
142
149
|
const payloadB64 = parts[1];
|
|
143
150
|
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
144
151
|
const payload = JSON.parse(atob(padded));
|
|
145
152
|
if (payload.sub) {
|
|
146
|
-
// Look up client to get the signing secret (secret hash)
|
|
147
153
|
const client = await authConfig.store.getClient(payload.sub);
|
|
148
154
|
if (client) {
|
|
149
155
|
const verified = await verifyJwt(credential, client.clientSecretHash);
|
|
@@ -174,7 +180,7 @@ async function resolveAuth(req, authConfig) {
|
|
|
174
180
|
isRoot: false,
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
|
-
function canSeeAgent(agent, auth) {
|
|
183
|
+
export function canSeeAgent(agent, auth) {
|
|
178
184
|
const visibility = (agent.visibility ??
|
|
179
185
|
agent.config?.visibility ??
|
|
180
186
|
"internal");
|
|
@@ -208,11 +214,11 @@ function getToolDefinitions() {
|
|
|
208
214
|
},
|
|
209
215
|
path: {
|
|
210
216
|
type: "string",
|
|
211
|
-
description: "Agent path (e.g
|
|
217
|
+
description: "Agent path (e.g., '@my-agent')",
|
|
212
218
|
},
|
|
213
219
|
tool: {
|
|
214
220
|
type: "string",
|
|
215
|
-
description: "Tool name to call
|
|
221
|
+
description: "Tool name to call",
|
|
216
222
|
},
|
|
217
223
|
params: {
|
|
218
224
|
type: "object",
|
|
@@ -241,29 +247,28 @@ function getToolDefinitions() {
|
|
|
241
247
|
// ============================================
|
|
242
248
|
export function createAgentServer(registry, options = {}) {
|
|
243
249
|
const { port = 3000, hostname = "localhost", basePath = "", cors = true, serverName = "agents-sdk", serverVersion = "1.0.0", secretStore, } = options;
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
// Signing keys for JWKS-based auth
|
|
251
|
+
const serverSigningKeys = [];
|
|
252
|
+
const configTrustedIssuers = options.trustedIssuers ?? [];
|
|
246
253
|
const authConfig = detectAuth(registry);
|
|
254
|
+
let serverInstance = null;
|
|
247
255
|
// ──────────────────────────────────────────
|
|
248
|
-
//
|
|
256
|
+
// JSON-RPC handler
|
|
249
257
|
// ──────────────────────────────────────────
|
|
250
258
|
async function handleJsonRpc(request, auth) {
|
|
251
259
|
switch (request.method) {
|
|
252
|
-
// MCP protocol handshake
|
|
253
260
|
case "initialize":
|
|
254
261
|
return jsonRpcSuccess(request.id, {
|
|
255
262
|
protocolVersion: "2024-11-05",
|
|
256
|
-
capabilities: { tools: {} },
|
|
263
|
+
capabilities: { tools: { listChanged: false } },
|
|
257
264
|
serverInfo: { name: serverName, version: serverVersion },
|
|
258
265
|
});
|
|
259
266
|
case "notifications/initialized":
|
|
260
267
|
return jsonRpcSuccess(request.id, {});
|
|
261
|
-
// List MCP tools
|
|
262
268
|
case "tools/list":
|
|
263
269
|
return jsonRpcSuccess(request.id, {
|
|
264
270
|
tools: getToolDefinitions(),
|
|
265
271
|
});
|
|
266
|
-
// Call an MCP tool
|
|
267
272
|
case "tools/call": {
|
|
268
273
|
const { name, arguments: args } = (request.params ?? {});
|
|
269
274
|
try {
|
|
@@ -271,7 +276,7 @@ export function createAgentServer(registry, options = {}) {
|
|
|
271
276
|
return jsonRpcSuccess(request.id, result);
|
|
272
277
|
}
|
|
273
278
|
catch (err) {
|
|
274
|
-
console.error("[server]
|
|
279
|
+
console.error("[server] Tool call error:", err);
|
|
275
280
|
return jsonRpcSuccess(request.id, mcpResult(`Error: ${err instanceof Error ? err.message : String(err)}`, true));
|
|
276
281
|
}
|
|
277
282
|
}
|
|
@@ -299,10 +304,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
299
304
|
req.callerType = "system";
|
|
300
305
|
}
|
|
301
306
|
// Process secret params: resolve refs, store raw secrets
|
|
302
|
-
// Auto-resolve secret:xxx refs in tool params before execution
|
|
303
307
|
if (req.params && secretStore) {
|
|
304
308
|
const ownerId = auth?.callerId ?? "anonymous";
|
|
305
|
-
// Find the tool schema to check for secret: true fields
|
|
306
309
|
const agent = registry.get(req.path);
|
|
307
310
|
const tool = agent?.tools.find((t) => t.name === req.tool);
|
|
308
311
|
const schema = tool?.inputSchema;
|
|
@@ -330,6 +333,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
330
333
|
return true;
|
|
331
334
|
if (tv === "public")
|
|
332
335
|
return true;
|
|
336
|
+
if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous")
|
|
337
|
+
return true;
|
|
333
338
|
if (tv === "internal" && auth)
|
|
334
339
|
return true;
|
|
335
340
|
return false;
|
|
@@ -343,7 +348,7 @@ export function createAgentServer(registry, options = {}) {
|
|
|
343
348
|
}
|
|
344
349
|
}
|
|
345
350
|
// ──────────────────────────────────────────
|
|
346
|
-
// OAuth2 token handler
|
|
351
|
+
// OAuth2 token handler
|
|
347
352
|
// ──────────────────────────────────────────
|
|
348
353
|
async function handleOAuthToken(req) {
|
|
349
354
|
if (!authConfig) {
|
|
@@ -378,521 +383,193 @@ export function createAgentServer(registry, options = {}) {
|
|
|
378
383
|
error_description: "Missing client_id or client_secret",
|
|
379
384
|
}, 400);
|
|
380
385
|
}
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
398
|
return jsonResponse({
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
399
|
+
access_token: tokenResult.accessToken,
|
|
400
|
+
token_type: "Bearer",
|
|
401
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
402
|
+
refresh_token: tokenResult.refreshToken,
|
|
403
|
+
});
|
|
387
404
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
},
|
|
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);
|
|
405
|
+
catch (err) {
|
|
406
|
+
console.error("[oauth] Token error:", err);
|
|
407
|
+
return jsonResponse({ error: "server_error", error_description: "Token exchange failed" }, 500);
|
|
409
408
|
}
|
|
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;
|
|
415
|
-
return jsonResponse({
|
|
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(" "),
|
|
420
|
-
});
|
|
421
409
|
}
|
|
422
410
|
// ──────────────────────────────────────────
|
|
423
|
-
//
|
|
424
|
-
//
|
|
411
|
+
// Main fetch handler
|
|
412
|
+
// ──────────────────────────────────────────
|
|
425
413
|
async function fetch(req) {
|
|
426
|
-
const url = new URL(req.url);
|
|
427
|
-
const path = url.pathname.replace(basePath, "") || "/";
|
|
428
|
-
// CORS preflight
|
|
429
|
-
if (cors && req.method === "OPTIONS") {
|
|
430
|
-
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
431
|
-
}
|
|
432
|
-
const addCors = (response) => {
|
|
433
|
-
if (!cors)
|
|
434
|
-
return response;
|
|
435
|
-
const headers = new Headers(response.headers);
|
|
436
|
-
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
437
|
-
headers.set(key, value);
|
|
438
|
-
}
|
|
439
|
-
return new Response(response.body, {
|
|
440
|
-
status: response.status,
|
|
441
|
-
statusText: response.statusText,
|
|
442
|
-
headers,
|
|
443
|
-
});
|
|
444
|
-
};
|
|
445
|
-
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
446
414
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
// OAuth2 token endpoint
|
|
454
|
-
if (path === "/oauth/token" && req.method === "POST") {
|
|
455
|
-
return addCors(await handleOAuthToken(req));
|
|
456
|
-
}
|
|
457
|
-
// Health check
|
|
458
|
-
if (path === "/health" && req.method === "GET") {
|
|
459
|
-
return addCors(jsonResponse({ status: "ok" }));
|
|
460
|
-
}
|
|
461
|
-
// Backwards compat: GET /list (returns agents directly)
|
|
462
|
-
if (path === "/list" && req.method === "GET") {
|
|
463
|
-
const agents = registry.list();
|
|
464
|
-
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
465
|
-
return addCors(jsonResponse({
|
|
466
|
-
success: true,
|
|
467
|
-
agents: visible.map((agent) => ({
|
|
468
|
-
path: agent.path,
|
|
469
|
-
name: agent.config?.name,
|
|
470
|
-
description: agent.config?.description,
|
|
471
|
-
supportedActions: agent.config?.supportedActions,
|
|
472
|
-
integration: agent.config?.integration || null,
|
|
473
|
-
tools: agent.tools
|
|
474
|
-
.filter((t) => {
|
|
475
|
-
const tv = t.visibility ?? "internal";
|
|
476
|
-
if (auth?.isRoot)
|
|
477
|
-
return true;
|
|
478
|
-
if (tv === "public")
|
|
479
|
-
return true;
|
|
480
|
-
if (tv === "internal" && auth)
|
|
481
|
-
return true;
|
|
482
|
-
return false;
|
|
483
|
-
})
|
|
484
|
-
.map((t) => t.name),
|
|
485
|
-
})),
|
|
486
|
-
}));
|
|
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() });
|
|
487
420
|
}
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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;
|
|
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
|
+
};
|
|
577
438
|
}
|
|
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
439
|
return null;
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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));
|
|
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);
|
|
659
448
|
}
|
|
660
|
-
//
|
|
661
|
-
if (path === "/
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
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;
|
|
665
453
|
}
|
|
666
|
-
// GET /
|
|
667
|
-
if (path === "/
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
}
|
|
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;
|
|
758
458
|
}
|
|
759
|
-
// GET /
|
|
760
|
-
if (path === "/
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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;
|
|
765
466
|
}
|
|
766
|
-
//
|
|
767
|
-
if (path === "/
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
},
|
|
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(),
|
|
830
478
|
});
|
|
479
|
+
return cors ? addCors(res) : res;
|
|
831
480
|
}
|
|
832
|
-
// GET /
|
|
833
|
-
if (path === "/
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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;
|
|
846
508
|
}
|
|
847
|
-
|
|
509
|
+
// ── Not found ──
|
|
510
|
+
const res = jsonResponse({
|
|
848
511
|
jsonrpc: "2.0",
|
|
849
512
|
id: null,
|
|
850
513
|
error: {
|
|
851
514
|
code: -32601,
|
|
852
515
|
message: `Not found: ${req.method} ${path}`,
|
|
853
516
|
},
|
|
854
|
-
}, 404)
|
|
517
|
+
}, 404);
|
|
518
|
+
return cors ? addCors(res) : res;
|
|
855
519
|
}
|
|
856
520
|
catch (err) {
|
|
857
521
|
console.error("[server] Request error:", err);
|
|
858
|
-
|
|
522
|
+
const res = jsonResponse({
|
|
859
523
|
jsonrpc: "2.0",
|
|
860
524
|
id: null,
|
|
861
|
-
error: {
|
|
862
|
-
|
|
525
|
+
error: {
|
|
526
|
+
code: -32603,
|
|
527
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
528
|
+
},
|
|
529
|
+
}, 500);
|
|
530
|
+
return cors ? addCors(res) : res;
|
|
863
531
|
}
|
|
864
532
|
}
|
|
865
533
|
// ──────────────────────────────────────────
|
|
866
534
|
// Server lifecycle
|
|
867
535
|
// ──────────────────────────────────────────
|
|
868
|
-
|
|
536
|
+
return {
|
|
537
|
+
url: null,
|
|
538
|
+
registry,
|
|
869
539
|
async start() {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
console.log(" POST /oauth/token - OAuth2 token endpoint");
|
|
880
|
-
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
|
+
}
|
|
881
549
|
}
|
|
882
|
-
|
|
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}`);
|
|
883
564
|
},
|
|
884
565
|
async stop() {
|
|
885
566
|
if (serverInstance) {
|
|
886
567
|
serverInstance.stop();
|
|
887
568
|
serverInstance = null;
|
|
888
|
-
|
|
569
|
+
this.url = null;
|
|
889
570
|
}
|
|
890
571
|
},
|
|
891
572
|
fetch,
|
|
892
|
-
get url() {
|
|
893
|
-
return serverUrl;
|
|
894
|
-
},
|
|
895
573
|
};
|
|
896
|
-
return server;
|
|
897
574
|
}
|
|
898
575
|
//# sourceMappingURL=server.js.map
|