@slashfi/agents-sdk 0.8.0 → 0.10.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 +236 -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 +272 -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
|
}
|
|
@@ -289,6 +292,10 @@ export function createAgentServer(registry, options = {}) {
|
|
|
289
292
|
case "call_agent": {
|
|
290
293
|
const req = (args.request ?? args);
|
|
291
294
|
// Inject auth context
|
|
295
|
+
// No auth = internal/trusted call (e.g., atlas-api → atlas-os RPC)
|
|
296
|
+
if (!auth) {
|
|
297
|
+
req.callerType = "system";
|
|
298
|
+
}
|
|
292
299
|
if (auth) {
|
|
293
300
|
req.callerId = auth.callerId;
|
|
294
301
|
req.callerType = auth.callerType;
|
|
@@ -301,10 +308,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
301
308
|
req.callerType = "system";
|
|
302
309
|
}
|
|
303
310
|
// Process secret params: resolve refs, store raw secrets
|
|
304
|
-
// Auto-resolve secret:xxx refs in tool params before execution
|
|
305
311
|
if (req.params && secretStore) {
|
|
306
312
|
const ownerId = auth?.callerId ?? "anonymous";
|
|
307
|
-
// Find the tool schema to check for secret: true fields
|
|
308
313
|
const agent = registry.get(req.path);
|
|
309
314
|
const tool = agent?.tools.find((t) => t.name === req.tool);
|
|
310
315
|
const schema = tool?.inputSchema;
|
|
@@ -332,6 +337,8 @@ export function createAgentServer(registry, options = {}) {
|
|
|
332
337
|
return true;
|
|
333
338
|
if (tv === "public")
|
|
334
339
|
return true;
|
|
340
|
+
if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous")
|
|
341
|
+
return true;
|
|
335
342
|
if (tv === "internal" && auth)
|
|
336
343
|
return true;
|
|
337
344
|
return false;
|
|
@@ -345,7 +352,7 @@ export function createAgentServer(registry, options = {}) {
|
|
|
345
352
|
}
|
|
346
353
|
}
|
|
347
354
|
// ──────────────────────────────────────────
|
|
348
|
-
// OAuth2 token handler
|
|
355
|
+
// OAuth2 token handler
|
|
349
356
|
// ──────────────────────────────────────────
|
|
350
357
|
async function handleOAuthToken(req) {
|
|
351
358
|
if (!authConfig) {
|
|
@@ -380,556 +387,193 @@ export function createAgentServer(registry, options = {}) {
|
|
|
380
387
|
error_description: "Missing client_id or client_secret",
|
|
381
388
|
}, 400);
|
|
382
389
|
}
|
|
383
|
-
|
|
384
|
-
|
|
390
|
+
try {
|
|
391
|
+
const result = await registry.call({
|
|
392
|
+
action: "execute_tool",
|
|
393
|
+
path: "@auth",
|
|
394
|
+
tool: "token",
|
|
395
|
+
params: { clientId, clientSecret },
|
|
396
|
+
callerType: "system",
|
|
397
|
+
});
|
|
398
|
+
const tokenResult = result?.result;
|
|
399
|
+
if (!tokenResult?.accessToken) {
|
|
400
|
+
return jsonResponse({ error: "invalid_client", error_description: "Authentication failed" }, 401);
|
|
401
|
+
}
|
|
385
402
|
return jsonResponse({
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
403
|
+
access_token: tokenResult.accessToken,
|
|
404
|
+
token_type: "Bearer",
|
|
405
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
406
|
+
refresh_token: tokenResult.refreshToken,
|
|
407
|
+
});
|
|
389
408
|
}
|
|
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);
|
|
409
|
+
catch (err) {
|
|
410
|
+
console.error("[oauth] Token error:", err);
|
|
411
|
+
return jsonResponse({ error: "server_error", error_description: "Token exchange failed" }, 500);
|
|
411
412
|
}
|
|
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
413
|
}
|
|
424
414
|
// ──────────────────────────────────────────
|
|
425
|
-
//
|
|
426
|
-
//
|
|
415
|
+
// Main fetch handler
|
|
416
|
+
// ──────────────────────────────────────────
|
|
427
417
|
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
418
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
// OAuth2 token endpoint
|
|
456
|
-
if (path === "/oauth/token" && req.method === "POST") {
|
|
457
|
-
return addCors(await handleOAuthToken(req));
|
|
458
|
-
}
|
|
459
|
-
// Health check
|
|
460
|
-
if (path === "/health" && req.method === "GET") {
|
|
461
|
-
return addCors(jsonResponse({ status: "ok" }));
|
|
462
|
-
}
|
|
463
|
-
// Backwards compat: GET /list (returns agents directly)
|
|
464
|
-
if (path === "/list" && req.method === "GET") {
|
|
465
|
-
const agents = registry.list();
|
|
466
|
-
const visible = agents.filter((agent) => canSeeAgent(agent, auth));
|
|
467
|
-
return addCors(jsonResponse({
|
|
468
|
-
success: true,
|
|
469
|
-
agents: visible.map((agent) => ({
|
|
470
|
-
path: agent.path,
|
|
471
|
-
name: agent.config?.name,
|
|
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
|
-
}));
|
|
419
|
+
const url = new URL(req.url);
|
|
420
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
421
|
+
// CORS preflight
|
|
422
|
+
if (cors && req.method === "OPTIONS") {
|
|
423
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
489
424
|
}
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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;
|
|
425
|
+
// Resolve auth for all requests
|
|
426
|
+
const auth = authConfig ? await resolveAuth(req, authConfig, {
|
|
427
|
+
signingKeys: serverSigningKeys,
|
|
428
|
+
trustedIssuers: configTrustedIssuers,
|
|
429
|
+
}) : null;
|
|
430
|
+
// Also check header-based identity (for proxied requests)
|
|
431
|
+
const headerAuth = !auth
|
|
432
|
+
? (() => {
|
|
433
|
+
const actorId = req.headers.get("X-Atlas-Actor-Id");
|
|
434
|
+
const actorType = req.headers.get("X-Atlas-Actor-Type");
|
|
435
|
+
if (actorId) {
|
|
436
|
+
return {
|
|
437
|
+
callerId: actorId,
|
|
438
|
+
callerType: actorType ?? "agent",
|
|
439
|
+
scopes: ["*"],
|
|
440
|
+
isRoot: false,
|
|
441
|
+
};
|
|
614
442
|
}
|
|
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
443
|
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));
|
|
444
|
+
})()
|
|
445
|
+
: null;
|
|
446
|
+
const effectiveAuth = auth ?? headerAuth;
|
|
447
|
+
// ── POST / → MCP JSON-RPC ──
|
|
448
|
+
if (path === "/" && req.method === "POST") {
|
|
449
|
+
const body = (await req.json());
|
|
450
|
+
const result = await handleJsonRpc(body, effectiveAuth);
|
|
451
|
+
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
696
452
|
}
|
|
697
|
-
//
|
|
698
|
-
if (path === "/
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return Response.redirect(slackAuthUrl(slackConfig), 302);
|
|
453
|
+
// ── POST /oauth/token → OAuth2 client_credentials ──
|
|
454
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
455
|
+
const res = await handleOAuthToken(req);
|
|
456
|
+
return cors ? addCors(res) : res;
|
|
702
457
|
}
|
|
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
|
-
}
|
|
458
|
+
// ── GET /health → Health check ──
|
|
459
|
+
if (path === "/health" && req.method === "GET") {
|
|
460
|
+
const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
|
|
461
|
+
return cors ? addCors(res) : res;
|
|
795
462
|
}
|
|
796
|
-
// GET /
|
|
797
|
-
if (path === "/
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
463
|
+
// ── GET /.well-known/jwks.json → JWKS public keys ──
|
|
464
|
+
if (path === "/.well-known/jwks.json" && req.method === "GET") {
|
|
465
|
+
const jwks = serverSigningKeys.length > 0
|
|
466
|
+
? await buildJwks(serverSigningKeys)
|
|
467
|
+
: { keys: [] };
|
|
468
|
+
const res = jsonResponse(jwks);
|
|
469
|
+
return cors ? addCors(res) : res;
|
|
802
470
|
}
|
|
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
|
-
},
|
|
471
|
+
// ── GET /.well-known/configuration → Server discovery ──
|
|
472
|
+
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
473
|
+
const baseUrl = new URL(req.url).origin;
|
|
474
|
+
const res = jsonResponse({
|
|
475
|
+
issuer: baseUrl,
|
|
476
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
477
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
478
|
+
agents_endpoint: `${baseUrl}/list`,
|
|
479
|
+
call_endpoint: baseUrl,
|
|
480
|
+
supported_grant_types: ["client_credentials"],
|
|
481
|
+
agents: registry.listPaths(),
|
|
867
482
|
});
|
|
483
|
+
return cors ? addCors(res) : res;
|
|
868
484
|
}
|
|
869
|
-
// GET /
|
|
870
|
-
if (path === "/
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
485
|
+
// ── GET /list → List agents (legacy endpoint) ──
|
|
486
|
+
if (path === "/list" && req.method === "GET") {
|
|
487
|
+
const agents = registry.list();
|
|
488
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
|
|
489
|
+
const res = jsonResponse(visible.map((agent) => ({
|
|
490
|
+
path: agent.path,
|
|
491
|
+
name: agent.config?.name,
|
|
492
|
+
description: agent.config?.description,
|
|
493
|
+
supportedActions: agent.config?.supportedActions,
|
|
494
|
+
integration: agent.config?.integration || null,
|
|
495
|
+
tools: agent.tools
|
|
496
|
+
.filter((t) => {
|
|
497
|
+
const tv = t.visibility ?? "internal";
|
|
498
|
+
if (effectiveAuth?.isRoot)
|
|
499
|
+
return true;
|
|
500
|
+
if (tv === "public")
|
|
501
|
+
return true;
|
|
502
|
+
if (tv === "internal" && effectiveAuth)
|
|
503
|
+
return true;
|
|
504
|
+
return false;
|
|
505
|
+
})
|
|
506
|
+
.map((t) => ({
|
|
507
|
+
name: t.name,
|
|
508
|
+
description: t.description,
|
|
509
|
+
})),
|
|
510
|
+
})));
|
|
511
|
+
return cors ? addCors(res) : res;
|
|
883
512
|
}
|
|
884
|
-
|
|
513
|
+
// ── Not found ──
|
|
514
|
+
const res = jsonResponse({
|
|
885
515
|
jsonrpc: "2.0",
|
|
886
516
|
id: null,
|
|
887
517
|
error: {
|
|
888
518
|
code: -32601,
|
|
889
519
|
message: `Not found: ${req.method} ${path}`,
|
|
890
520
|
},
|
|
891
|
-
}, 404)
|
|
521
|
+
}, 404);
|
|
522
|
+
return cors ? addCors(res) : res;
|
|
892
523
|
}
|
|
893
524
|
catch (err) {
|
|
894
525
|
console.error("[server] Request error:", err);
|
|
895
|
-
|
|
526
|
+
const res = jsonResponse({
|
|
896
527
|
jsonrpc: "2.0",
|
|
897
528
|
id: null,
|
|
898
|
-
error: {
|
|
899
|
-
|
|
529
|
+
error: {
|
|
530
|
+
code: -32603,
|
|
531
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
532
|
+
},
|
|
533
|
+
}, 500);
|
|
534
|
+
return cors ? addCors(res) : res;
|
|
900
535
|
}
|
|
901
536
|
}
|
|
902
537
|
// ──────────────────────────────────────────
|
|
903
538
|
// Server lifecycle
|
|
904
539
|
// ──────────────────────────────────────────
|
|
905
|
-
|
|
540
|
+
return {
|
|
541
|
+
url: null,
|
|
542
|
+
registry,
|
|
906
543
|
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");
|
|
544
|
+
// Load or generate signing key for JWKS
|
|
545
|
+
if (options.signingKey) {
|
|
546
|
+
serverSigningKeys.push(options.signingKey);
|
|
547
|
+
}
|
|
548
|
+
else if (authConfig?.store?.getSigningKeys) {
|
|
549
|
+
const stored = await authConfig.store.getSigningKeys() ?? [];
|
|
550
|
+
for (const exported of stored) {
|
|
551
|
+
serverSigningKeys.push(await importSigningKey(exported));
|
|
552
|
+
}
|
|
918
553
|
}
|
|
919
|
-
|
|
554
|
+
if (serverSigningKeys.length === 0) {
|
|
555
|
+
const key = await generateSigningKey();
|
|
556
|
+
serverSigningKeys.push(key);
|
|
557
|
+
if (authConfig?.store?.storeSigningKey) {
|
|
558
|
+
await authConfig.store.storeSigningKey(await exportSigningKey(key));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
serverInstance = Bun.serve({
|
|
562
|
+
port,
|
|
563
|
+
hostname,
|
|
564
|
+
fetch,
|
|
565
|
+
});
|
|
566
|
+
this.url = `http://${hostname}:${port}`;
|
|
567
|
+
console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
|
|
920
568
|
},
|
|
921
569
|
async stop() {
|
|
922
570
|
if (serverInstance) {
|
|
923
571
|
serverInstance.stop();
|
|
924
572
|
serverInstance = null;
|
|
925
|
-
|
|
573
|
+
this.url = null;
|
|
926
574
|
}
|
|
927
575
|
},
|
|
928
576
|
fetch,
|
|
929
|
-
get url() {
|
|
930
|
-
return serverUrl;
|
|
931
|
-
},
|
|
932
577
|
};
|
|
933
|
-
return server;
|
|
934
578
|
}
|
|
935
579
|
//# sourceMappingURL=server.js.map
|