@slashfi/agents-sdk 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-definitions/auth.d.ts +17 -0
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +135 -1
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +19 -0
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +218 -5
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/integration-interface.d.ts +37 -0
- package/dist/integration-interface.d.ts.map +1 -0
- package/dist/integration-interface.js +94 -0
- package/dist/integration-interface.js.map +1 -0
- package/dist/integrations-store.d.ts +33 -0
- package/dist/integrations-store.d.ts.map +1 -0
- package/dist/integrations-store.js +50 -0
- package/dist/integrations-store.js.map +1 -0
- package/dist/jwt.d.ts +86 -17
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +140 -17
- package/dist/jwt.js.map +1 -1
- package/dist/registry.d.ts +7 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -21
- package/dist/registry.js.map +1 -1
- package/dist/secret-collection.d.ts +37 -0
- package/dist/secret-collection.d.ts.map +1 -0
- package/dist/secret-collection.js +37 -0
- package/dist/secret-collection.js.map +1 -0
- package/dist/server.d.ts +41 -44
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +232 -592
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/agent-definitions/auth.ts +187 -1
- package/src/agent-definitions/integrations.ts +260 -5
- package/src/index.ts +18 -4
- package/src/integration-interface.ts +118 -0
- package/src/integrations-store.ts +84 -0
- package/src/jwt.ts +233 -65
- package/src/registry.ts +17 -2
- package/src/secret-collection.ts +66 -0
- package/src/server.ts +268 -681
- package/src/types.ts +8 -1
- package/dist/slack-oauth.d.ts +0 -27
- package/dist/slack-oauth.d.ts.map +0 -1
- package/dist/slack-oauth.js +0 -48
- package/dist/slack-oauth.js.map +0 -1
- package/dist/web-pages.d.ts +0 -8
- package/dist/web-pages.d.ts.map +0 -1
- package/dist/web-pages.js +0 -169
- package/dist/web-pages.js.map +0 -1
- package/src/slack-oauth.ts +0 -66
- package/src/web-pages.ts +0 -178
package/src/server.ts
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
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
|
|
|
30
28
|
import type { AuthStore } from "./agent-definitions/auth.js";
|
|
@@ -33,17 +31,10 @@ import {
|
|
|
33
31
|
processSecretParams,
|
|
34
32
|
} from "./agent-definitions/secrets.js";
|
|
35
33
|
import { verifyJwt } from "./jwt.js";
|
|
34
|
+
import type { SigningKey } from "./jwt.js";
|
|
35
|
+
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
|
|
36
36
|
import type { AgentRegistry } from "./registry.js";
|
|
37
37
|
import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
|
|
38
|
-
import { renderLoginPage, renderDashboardPage, renderTenantPage } from "./web-pages.js";
|
|
39
|
-
import { slackAuthUrl, exchangeSlackCode, getSlackProfile, type SlackOAuthConfig } from "./slack-oauth.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function resolveBaseUrl(req: Request, url: URL): string {
|
|
43
|
-
const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
|
|
44
|
-
const host = req.headers.get("x-forwarded-host") || url.host;
|
|
45
|
-
return `${proto}://${host}`;
|
|
46
|
-
}
|
|
47
38
|
|
|
48
39
|
// ============================================
|
|
49
40
|
// Server Types
|
|
@@ -62,9 +53,12 @@ export interface AgentServerOptions {
|
|
|
62
53
|
serverName?: string;
|
|
63
54
|
/** Server version reported in MCP initialize (default: '1.0.0') */
|
|
64
55
|
serverVersion?: string;
|
|
65
|
-
|
|
66
56
|
/** Secret store for handling secret: refs in tool params */
|
|
67
57
|
secretStore?: SecretStore;
|
|
58
|
+
/** URLs of trusted registries for cross-registry JWT verification */
|
|
59
|
+
trustedIssuers?: string[];
|
|
60
|
+
/** Pre-generated signing key (if not provided, one is generated on start) */
|
|
61
|
+
signingKey?: SigningKey;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
export interface AgentServer {
|
|
@@ -72,10 +66,12 @@ export interface AgentServer {
|
|
|
72
66
|
start(): Promise<void>;
|
|
73
67
|
/** Stop the server */
|
|
74
68
|
stop(): Promise<void>;
|
|
75
|
-
/** Handle a request (for custom integrations) */
|
|
69
|
+
/** Handle a request (for custom integrations / framework composition) */
|
|
76
70
|
fetch(req: Request): Promise<Response>;
|
|
77
71
|
/** Get the server URL (only available after start) */
|
|
78
72
|
url: string | null;
|
|
73
|
+
/** The agent registry this server uses */
|
|
74
|
+
registry: AgentRegistry;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
// ============================================
|
|
@@ -97,79 +93,24 @@ interface JsonRpcResponse {
|
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
// ============================================
|
|
100
|
-
// Auth Types
|
|
96
|
+
// Auth Types (exported for use by custom routes)
|
|
101
97
|
// ============================================
|
|
102
98
|
|
|
103
|
-
interface AuthConfig {
|
|
99
|
+
export interface AuthConfig {
|
|
104
100
|
store: AuthStore;
|
|
105
101
|
rootKey: string;
|
|
106
102
|
tokenTtl: number;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
interface ResolvedAuth {
|
|
105
|
+
export interface ResolvedAuth {
|
|
110
106
|
callerId: string;
|
|
111
107
|
callerType: "agent" | "user" | "system";
|
|
112
108
|
scopes: string[];
|
|
113
109
|
isRoot: boolean;
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
// ============================================
|
|
118
|
-
// Secrets Collection (one-time tokens)
|
|
119
|
-
// ============================================
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
function escHtml(s: string): string {
|
|
123
|
-
return s.replace(/&/g,"\&").replace(/</g,"\<").replace(/>/g,"\>").replace(/"/g,"\"");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function renderSecretForm(token: string, pending: PendingCollection, baseUrl: string): string {
|
|
127
|
-
const fields = pending.fields.map(f => `
|
|
128
|
-
<div class="field">
|
|
129
|
-
<label>${escHtml(f.name)}${f.secret ? ` <span class="badge">SECRET</span>` : ""}${f.required ? ` <span class="req">*</span>` : ""}</label>
|
|
130
|
-
${f.description ? `<p class="desc">${escHtml(f.description)}</p>` : ""}
|
|
131
|
-
<input type="${f.secret ? "password" : "text"}" name="${escHtml(f.name)}" ${f.required ? "required" : ""} autocomplete="off" />
|
|
132
|
-
</div>`).join("");
|
|
133
|
-
|
|
134
|
-
return `<!DOCTYPE html>
|
|
135
|
-
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Secure Setup</title>
|
|
136
|
-
<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>
|
|
137
|
-
<div class="card" id="fc"><div class="header"><span class="lock">🔐</span><h1>${escHtml(pending.tool)} on ${escHtml(pending.agent)}</h1></div>
|
|
138
|
-
<p class="subtitle">Enter credentials below. They are encrypted and stored securely — they never pass through the AI.</p>
|
|
139
|
-
<div class="shield">🛡️ End-to-end encrypted</div><div id="err" class="error"></div>
|
|
140
|
-
<form id="f">${fields}<button type="submit">Submit Securely</button></form>
|
|
141
|
-
<p class="footer">Expires in 10 minutes</p></div>
|
|
142
|
-
<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>
|
|
143
|
-
<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>`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface PendingCollection {
|
|
147
|
-
/** Partial params already provided by agent */
|
|
148
|
-
params: Record<string, unknown>;
|
|
149
|
-
/** Target agent + tool to call after collection */
|
|
150
|
-
agent: string;
|
|
151
|
-
tool: string;
|
|
152
|
-
/** Auth context from original request */
|
|
153
|
-
auth: ResolvedAuth | null;
|
|
154
|
-
/** Fields the form needs to collect */
|
|
155
|
-
fields: Array<{ name: string; description?: string; secret: boolean; required: boolean }>;
|
|
156
|
-
/** Created timestamp for expiry */
|
|
157
|
-
createdAt: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const pendingCollections = new Map<string, PendingCollection>();
|
|
161
|
-
|
|
162
|
-
export function generateCollectionToken(): string {
|
|
163
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
164
|
-
let token = "sc_";
|
|
165
|
-
for (let i = 0; i < 32; i++) {
|
|
166
|
-
token += chars[Math.floor(Math.random() * chars.length)];
|
|
167
|
-
}
|
|
168
|
-
return token;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
112
|
// ============================================
|
|
172
|
-
// Helpers
|
|
113
|
+
// HTTP Helpers
|
|
173
114
|
// ============================================
|
|
174
115
|
|
|
175
116
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
@@ -184,10 +125,22 @@ function corsHeaders(): Record<string, string> {
|
|
|
184
125
|
"Access-Control-Allow-Origin": "*",
|
|
185
126
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
186
127
|
"Access-Control-Allow-Headers":
|
|
187
|
-
"Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-
|
|
128
|
+
"Content-Type, Authorization, X-Atlas-Actor-Id, X-Atlas-Actor-Type",
|
|
188
129
|
};
|
|
189
130
|
}
|
|
190
131
|
|
|
132
|
+
function addCors(res: Response): Response {
|
|
133
|
+
const headers = new Headers(res.headers);
|
|
134
|
+
for (const [k, v] of Object.entries(corsHeaders())) {
|
|
135
|
+
headers.set(k, v);
|
|
136
|
+
}
|
|
137
|
+
return new Response(res.body, { status: res.status, headers });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// JSON-RPC Helpers
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
191
144
|
function jsonRpcSuccess(id: unknown, result: unknown): JsonRpcResponse {
|
|
192
145
|
return { jsonrpc: "2.0", id, result };
|
|
193
146
|
}
|
|
@@ -220,10 +173,10 @@ function mcpResult(value: unknown, isError = false) {
|
|
|
220
173
|
}
|
|
221
174
|
|
|
222
175
|
// ============================================
|
|
223
|
-
// Auth Detection
|
|
176
|
+
// Auth Detection & Resolution
|
|
224
177
|
// ============================================
|
|
225
178
|
|
|
226
|
-
function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
179
|
+
export function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
227
180
|
const authAgent = registry.get("@auth") as
|
|
228
181
|
| (AgentDefinition & {
|
|
229
182
|
__authStore?: AuthStore;
|
|
@@ -241,9 +194,10 @@ function detectAuth(registry: AgentRegistry): AuthConfig | null {
|
|
|
241
194
|
};
|
|
242
195
|
}
|
|
243
196
|
|
|
244
|
-
async function resolveAuth(
|
|
197
|
+
export async function resolveAuth(
|
|
245
198
|
req: Request,
|
|
246
199
|
authConfig: AuthConfig,
|
|
200
|
+
jwksOptions?: { signingKeys?: SigningKey[]; trustedIssuers?: string[] },
|
|
247
201
|
): Promise<ResolvedAuth | null> {
|
|
248
202
|
const authHeader = req.headers.get("Authorization");
|
|
249
203
|
if (!authHeader) return null;
|
|
@@ -251,6 +205,7 @@ async function resolveAuth(
|
|
|
251
205
|
const [scheme, credential] = authHeader.split(" ", 2);
|
|
252
206
|
if (scheme?.toLowerCase() !== "bearer" || !credential) return null;
|
|
253
207
|
|
|
208
|
+
// Root key check
|
|
254
209
|
if (credential === authConfig.rootKey) {
|
|
255
210
|
return {
|
|
256
211
|
callerId: "root",
|
|
@@ -260,12 +215,47 @@ async function resolveAuth(
|
|
|
260
215
|
};
|
|
261
216
|
}
|
|
262
217
|
|
|
263
|
-
// Try
|
|
264
|
-
// JWT is signed with the client's secret hash
|
|
265
|
-
// Decode payload to get client_id, look up client, verify signature
|
|
218
|
+
// Try ES256 verification against own signing keys
|
|
266
219
|
const parts = credential.split(".");
|
|
220
|
+
if (parts.length === 3 && jwksOptions?.signingKeys?.length) {
|
|
221
|
+
for (const key of jwksOptions.signingKeys) {
|
|
222
|
+
try {
|
|
223
|
+
const verified = await verifyJwtLocal(credential, key.publicKey);
|
|
224
|
+
if (verified) {
|
|
225
|
+
return {
|
|
226
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
227
|
+
callerType: "agent",
|
|
228
|
+
scopes: verified.scopes ?? ["*"],
|
|
229
|
+
isRoot: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try trusted issuers (remote JWKS verification)
|
|
239
|
+
if (parts.length === 3 && jwksOptions?.trustedIssuers?.length) {
|
|
240
|
+
for (const issuer of jwksOptions.trustedIssuers) {
|
|
241
|
+
try {
|
|
242
|
+
const verified = await verifyJwtFromIssuer(credential, issuer);
|
|
243
|
+
if (verified) {
|
|
244
|
+
return {
|
|
245
|
+
callerId: verified.sub ?? verified.name ?? "unknown",
|
|
246
|
+
callerType: "agent",
|
|
247
|
+
scopes: verified.scopes ?? ["*"],
|
|
248
|
+
isRoot: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Try HMAC JWT verification (legacy, stateless)
|
|
267
258
|
if (parts.length === 3) {
|
|
268
|
-
// Looks like a JWT - decode payload to get client_id
|
|
269
259
|
try {
|
|
270
260
|
const payloadB64 = parts[1];
|
|
271
261
|
const padded = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -277,7 +267,6 @@ async function resolveAuth(
|
|
|
277
267
|
};
|
|
278
268
|
|
|
279
269
|
if (payload.sub) {
|
|
280
|
-
// Look up client to get the signing secret (secret hash)
|
|
281
270
|
const client = await authConfig.store.getClient(payload.sub);
|
|
282
271
|
if (client) {
|
|
283
272
|
const verified = await verifyJwt(credential, client.clientSecretHash);
|
|
@@ -309,7 +298,7 @@ async function resolveAuth(
|
|
|
309
298
|
};
|
|
310
299
|
}
|
|
311
300
|
|
|
312
|
-
function canSeeAgent(
|
|
301
|
+
export function canSeeAgent(
|
|
313
302
|
agent: AgentDefinition,
|
|
314
303
|
auth: ResolvedAuth | null,
|
|
315
304
|
): boolean {
|
|
@@ -346,11 +335,11 @@ function getToolDefinitions() {
|
|
|
346
335
|
},
|
|
347
336
|
path: {
|
|
348
337
|
type: "string",
|
|
349
|
-
description: "Agent path (e.g
|
|
338
|
+
description: "Agent path (e.g., '@my-agent')",
|
|
350
339
|
},
|
|
351
340
|
tool: {
|
|
352
341
|
type: "string",
|
|
353
|
-
description: "Tool name to call
|
|
342
|
+
description: "Tool name to call",
|
|
354
343
|
},
|
|
355
344
|
params: {
|
|
356
345
|
type: "object",
|
|
@@ -366,7 +355,8 @@ function getToolDefinitions() {
|
|
|
366
355
|
},
|
|
367
356
|
{
|
|
368
357
|
name: "list_agents",
|
|
369
|
-
description:
|
|
358
|
+
description:
|
|
359
|
+
"List all registered agents and their available tools.",
|
|
370
360
|
inputSchema: {
|
|
371
361
|
type: "object",
|
|
372
362
|
properties: {},
|
|
@@ -393,13 +383,15 @@ export function createAgentServer(
|
|
|
393
383
|
secretStore,
|
|
394
384
|
} = options;
|
|
395
385
|
|
|
396
|
-
|
|
397
|
-
|
|
386
|
+
// Signing keys for JWKS-based auth
|
|
387
|
+
const serverSigningKeys: SigningKey[] = [];
|
|
388
|
+
const configTrustedIssuers: string[] = options.trustedIssuers ?? [];
|
|
398
389
|
|
|
399
390
|
const authConfig = detectAuth(registry);
|
|
391
|
+
let serverInstance: ReturnType<typeof Bun.serve> | null = null;
|
|
400
392
|
|
|
401
393
|
// ──────────────────────────────────────────
|
|
402
|
-
//
|
|
394
|
+
// JSON-RPC handler
|
|
403
395
|
// ──────────────────────────────────────────
|
|
404
396
|
|
|
405
397
|
async function handleJsonRpc(
|
|
@@ -407,24 +399,21 @@ export function createAgentServer(
|
|
|
407
399
|
auth: ResolvedAuth | null,
|
|
408
400
|
): Promise<JsonRpcResponse> {
|
|
409
401
|
switch (request.method) {
|
|
410
|
-
// MCP protocol handshake
|
|
411
402
|
case "initialize":
|
|
412
403
|
return jsonRpcSuccess(request.id, {
|
|
413
404
|
protocolVersion: "2024-11-05",
|
|
414
|
-
capabilities: { tools: {} },
|
|
405
|
+
capabilities: { tools: { listChanged: false } },
|
|
415
406
|
serverInfo: { name: serverName, version: serverVersion },
|
|
416
407
|
});
|
|
417
408
|
|
|
418
409
|
case "notifications/initialized":
|
|
419
410
|
return jsonRpcSuccess(request.id, {});
|
|
420
411
|
|
|
421
|
-
// List MCP tools
|
|
422
412
|
case "tools/list":
|
|
423
413
|
return jsonRpcSuccess(request.id, {
|
|
424
414
|
tools: getToolDefinitions(),
|
|
425
415
|
});
|
|
426
416
|
|
|
427
|
-
// Call an MCP tool
|
|
428
417
|
case "tools/call": {
|
|
429
418
|
const { name, arguments: args } = (request.params ?? {}) as {
|
|
430
419
|
name: string;
|
|
@@ -435,7 +424,7 @@ export function createAgentServer(
|
|
|
435
424
|
const result = await handleToolCall(name, args ?? {}, auth);
|
|
436
425
|
return jsonRpcSuccess(request.id, result);
|
|
437
426
|
} catch (err) {
|
|
438
|
-
console.error("[server]
|
|
427
|
+
console.error("[server] Tool call error:", err);
|
|
439
428
|
return jsonRpcSuccess(
|
|
440
429
|
request.id,
|
|
441
430
|
mcpResult(
|
|
@@ -481,10 +470,8 @@ export function createAgentServer(
|
|
|
481
470
|
}
|
|
482
471
|
|
|
483
472
|
// Process secret params: resolve refs, store raw secrets
|
|
484
|
-
// Auto-resolve secret:xxx refs in tool params before execution
|
|
485
473
|
if ((req as any).params && secretStore) {
|
|
486
474
|
const ownerId = auth?.callerId ?? "anonymous";
|
|
487
|
-
// Find the tool schema to check for secret: true fields
|
|
488
475
|
const agent = registry.get(req.path);
|
|
489
476
|
const tool = agent?.tools.find((t) => t.name === (req as any).tool);
|
|
490
477
|
const schema = tool?.inputSchema as any;
|
|
@@ -518,6 +505,7 @@ export function createAgentServer(
|
|
|
518
505
|
const tv = t.visibility ?? "internal";
|
|
519
506
|
if (auth?.isRoot) return true;
|
|
520
507
|
if (tv === "public") return true;
|
|
508
|
+
if (tv === "authenticated" && auth?.callerId && auth.callerId !== "anonymous") return true;
|
|
521
509
|
if (tv === "internal" && auth) return true;
|
|
522
510
|
return false;
|
|
523
511
|
})
|
|
@@ -532,7 +520,7 @@ export function createAgentServer(
|
|
|
532
520
|
}
|
|
533
521
|
|
|
534
522
|
// ──────────────────────────────────────────
|
|
535
|
-
// OAuth2 token handler
|
|
523
|
+
// OAuth2 token handler
|
|
536
524
|
// ──────────────────────────────────────────
|
|
537
525
|
|
|
538
526
|
async function handleOAuthToken(req: Request): Promise<Response> {
|
|
@@ -578,583 +566,175 @@ export function createAgentServer(
|
|
|
578
566
|
);
|
|
579
567
|
}
|
|
580
568
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
{
|
|
588
|
-
error: "invalid_client",
|
|
589
|
-
error_description: "Invalid client credentials",
|
|
590
|
-
},
|
|
591
|
-
401,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Delegate to @auth agent's token tool which generates proper JWTs
|
|
596
|
-
const tokenResult = await registry.call({
|
|
597
|
-
action: "execute_tool",
|
|
598
|
-
path: "@auth",
|
|
599
|
-
tool: "token",
|
|
600
|
-
params: {
|
|
601
|
-
grantType: "client_credentials",
|
|
602
|
-
clientId,
|
|
603
|
-
clientSecret,
|
|
604
|
-
},
|
|
605
|
-
context: {
|
|
606
|
-
tenantId: "default",
|
|
607
|
-
agentPath: "@auth",
|
|
608
|
-
callerId: "oauth_endpoint",
|
|
569
|
+
try {
|
|
570
|
+
const result = await registry.call({
|
|
571
|
+
action: "execute_tool",
|
|
572
|
+
path: "@auth",
|
|
573
|
+
tool: "token",
|
|
574
|
+
params: { clientId, clientSecret },
|
|
609
575
|
callerType: "system",
|
|
610
|
-
}
|
|
611
|
-
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const tokenResult = (result as any)?.result;
|
|
579
|
+
if (!tokenResult?.accessToken) {
|
|
580
|
+
return jsonResponse(
|
|
581
|
+
{ error: "invalid_client", error_description: "Authentication failed" },
|
|
582
|
+
401,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
612
585
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
586
|
+
return jsonResponse({
|
|
587
|
+
access_token: tokenResult.accessToken,
|
|
588
|
+
token_type: "Bearer",
|
|
589
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
590
|
+
refresh_token: tokenResult.refreshToken,
|
|
591
|
+
});
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error("[oauth] Token error:", err);
|
|
594
|
+
return jsonResponse(
|
|
595
|
+
{ error: "server_error", error_description: "Token exchange failed" },
|
|
596
|
+
500,
|
|
597
|
+
);
|
|
617
598
|
}
|
|
618
|
-
const tokenData = callResponse.result;
|
|
619
|
-
|
|
620
|
-
// accessToken may be wrapped as { $agent_type: "secret", value: "<jwt>" }
|
|
621
|
-
const accessToken = tokenData.accessToken?.$agent_type === "secret"
|
|
622
|
-
? tokenData.accessToken.value
|
|
623
|
-
: tokenData.accessToken;
|
|
624
|
-
|
|
625
|
-
return jsonResponse({
|
|
626
|
-
access_token: accessToken,
|
|
627
|
-
token_type: tokenData.tokenType ?? "Bearer",
|
|
628
|
-
expires_in: tokenData.expiresIn ?? authConfig.tokenTtl,
|
|
629
|
-
scope: Array.isArray(tokenData.scopes) ? tokenData.scopes.join(" ") : client.scopes.join(" "),
|
|
630
|
-
});
|
|
631
599
|
}
|
|
632
600
|
|
|
633
601
|
// ──────────────────────────────────────────
|
|
634
|
-
//
|
|
635
|
-
//
|
|
602
|
+
// Main fetch handler
|
|
603
|
+
// ──────────────────────────────────────────
|
|
636
604
|
|
|
637
605
|
async function fetch(req: Request): Promise<Response> {
|
|
638
|
-
const url = new URL(req.url);
|
|
639
|
-
const path = url.pathname.replace(basePath, "") || "/";
|
|
640
|
-
|
|
641
|
-
// CORS preflight
|
|
642
|
-
if (cors && req.method === "OPTIONS") {
|
|
643
|
-
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const addCors = (response: Response): Response => {
|
|
647
|
-
if (!cors) return response;
|
|
648
|
-
const headers = new Headers(response.headers);
|
|
649
|
-
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
650
|
-
headers.set(key, value);
|
|
651
|
-
}
|
|
652
|
-
return new Response(response.body, {
|
|
653
|
-
status: response.status,
|
|
654
|
-
statusText: response.statusText,
|
|
655
|
-
headers,
|
|
656
|
-
});
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
const auth = authConfig ? await resolveAuth(req, authConfig) : null;
|
|
660
|
-
|
|
661
606
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const body = (await req.json()) as JsonRpcRequest;
|
|
665
|
-
const response = await handleJsonRpc(body, auth);
|
|
666
|
-
return addCors(jsonResponse(response));
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// OAuth2 token endpoint
|
|
670
|
-
if (path === "/oauth/token" && req.method === "POST") {
|
|
671
|
-
return addCors(await handleOAuthToken(req));
|
|
672
|
-
}
|
|
607
|
+
const url = new URL(req.url);
|
|
608
|
+
const path = url.pathname.replace(basePath, "") || "/";
|
|
673
609
|
|
|
674
|
-
//
|
|
675
|
-
if (
|
|
676
|
-
return
|
|
610
|
+
// CORS preflight
|
|
611
|
+
if (cors && req.method === "OPTIONS") {
|
|
612
|
+
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
677
613
|
}
|
|
678
614
|
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (tv === "internal" && auth) return true;
|
|
698
|
-
return false;
|
|
699
|
-
})
|
|
700
|
-
.map((t) => t.name),
|
|
701
|
-
})),
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
// ---- Shared OAuth callback handler ----
|
|
708
|
-
async function handleIntegrationOAuthCallback(provider: string, req: Request): Promise<Response> {
|
|
709
|
-
const url = new URL(req.url);
|
|
710
|
-
const code = url.searchParams.get("code");
|
|
711
|
-
const state = url.searchParams.get("state");
|
|
712
|
-
const oauthError = url.searchParams.get("error");
|
|
713
|
-
const errorDescription = url.searchParams.get("error_description");
|
|
714
|
-
|
|
715
|
-
if (oauthError) {
|
|
716
|
-
return new Response(
|
|
717
|
-
`<html><body><h1>Authorization Failed</h1><p>${errorDescription ?? oauthError}</p></body></html>`,
|
|
718
|
-
{ status: 400, headers: { "Content-Type": "text/html", ...corsHeaders() } },
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (!code) {
|
|
723
|
-
return addCors(jsonResponse({ error: "Missing authorization code" }, 400));
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
await registry.call({
|
|
728
|
-
action: "execute_tool",
|
|
729
|
-
path: "@integrations",
|
|
730
|
-
tool: "handle_oauth_callback",
|
|
731
|
-
params: { provider, code, state: state ?? undefined },
|
|
732
|
-
context: {
|
|
733
|
-
tenantId: "default",
|
|
734
|
-
agentPath: "@integrations",
|
|
735
|
-
callerId: "oauth_callback",
|
|
736
|
-
callerType: "system",
|
|
737
|
-
},
|
|
738
|
-
} as any);
|
|
739
|
-
|
|
740
|
-
// Parse redirect URL from state (base64-encoded JSON)
|
|
741
|
-
let redirectUrl = "/";
|
|
742
|
-
if (state) {
|
|
743
|
-
try {
|
|
744
|
-
const parsed = JSON.parse(atob(state));
|
|
745
|
-
if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
|
|
746
|
-
} catch {
|
|
747
|
-
// Fallback: try raw JSON for backward compat
|
|
748
|
-
try {
|
|
749
|
-
const parsed = JSON.parse(state);
|
|
750
|
-
if (parsed.redirectUrl) redirectUrl = parsed.redirectUrl;
|
|
751
|
-
} catch {}
|
|
615
|
+
// Resolve auth for all requests
|
|
616
|
+
const auth = authConfig ? await resolveAuth(req, authConfig, {
|
|
617
|
+
signingKeys: serverSigningKeys,
|
|
618
|
+
trustedIssuers: configTrustedIssuers,
|
|
619
|
+
}) : null;
|
|
620
|
+
|
|
621
|
+
// Also check header-based identity (for proxied requests)
|
|
622
|
+
const headerAuth: ResolvedAuth | null = !auth
|
|
623
|
+
? (() => {
|
|
624
|
+
const actorId = req.headers.get("X-Atlas-Actor-Id");
|
|
625
|
+
const actorType = req.headers.get("X-Atlas-Actor-Type");
|
|
626
|
+
if (actorId) {
|
|
627
|
+
return {
|
|
628
|
+
callerId: actorId,
|
|
629
|
+
callerType: (actorType as any) ?? "agent",
|
|
630
|
+
scopes: ["*"],
|
|
631
|
+
isRoot: false,
|
|
632
|
+
};
|
|
752
633
|
}
|
|
753
|
-
|
|
634
|
+
return null;
|
|
635
|
+
})()
|
|
636
|
+
: null;
|
|
754
637
|
|
|
755
|
-
|
|
756
|
-
return Response.redirect(`${redirectUrl}${sep}connected=${provider}`, 302);
|
|
757
|
-
} catch (err) {
|
|
758
|
-
return new Response(
|
|
759
|
-
`<html><body><h1>Connection Failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`,
|
|
760
|
-
{ status: 500, headers: { "Content-Type": "text/html", ...corsHeaders() } },
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
638
|
+
const effectiveAuth = auth ?? headerAuth;
|
|
764
639
|
|
|
765
|
-
//
|
|
766
|
-
if (path === "/
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
if (state) {
|
|
771
|
-
try {
|
|
772
|
-
const parsed = JSON.parse(atob(state));
|
|
773
|
-
provider = parsed.providerId;
|
|
774
|
-
} catch {
|
|
775
|
-
// Fallback: try raw JSON for backward compat
|
|
776
|
-
try {
|
|
777
|
-
const parsed = JSON.parse(state);
|
|
778
|
-
provider = parsed.providerId;
|
|
779
|
-
} catch {}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (!provider) {
|
|
783
|
-
return addCors(jsonResponse({ error: "Missing provider in state param" }, 400));
|
|
784
|
-
}
|
|
785
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// GET /integrations/callback/:provider - Legacy OAuth callback (provider from URL path)
|
|
789
|
-
if (path.startsWith("/integrations/callback/") && req.method === "GET") {
|
|
790
|
-
const provider = path.split("/integrations/callback/")[1]?.split("?")[0];
|
|
791
|
-
if (!provider) {
|
|
792
|
-
return addCors(jsonResponse({ error: "Missing provider" }, 400));
|
|
793
|
-
}
|
|
794
|
-
return handleIntegrationOAuthCallback(provider, req);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
// GET /secrets/form/:token - Serve hosted secrets form
|
|
799
|
-
if (path.startsWith("/secrets/form/") && req.method === "GET") {
|
|
800
|
-
const token = path.split("/").pop() ?? "";
|
|
801
|
-
const pending = pendingCollections.get(token);
|
|
802
|
-
if (!pending) {
|
|
803
|
-
return addCors(new Response("Invalid or expired form link", { status: 404 }));
|
|
804
|
-
}
|
|
805
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
806
|
-
pendingCollections.delete(token);
|
|
807
|
-
return addCors(new Response("Form link expired", { status: 410 }));
|
|
808
|
-
}
|
|
809
|
-
const reqUrl = new URL(req.url); const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
810
|
-
const html = renderSecretForm(token, pending, baseUrl);
|
|
811
|
-
return addCors(new Response(html, { headers: { "Content-Type": "text/html" } }));
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// POST /secrets/collect - Submit collected secrets and auto-forward to tool
|
|
815
|
-
if (path === "/secrets/collect" && req.method === "POST") {
|
|
816
|
-
const body = (await req.json()) as {
|
|
817
|
-
token: string;
|
|
818
|
-
values: Record<string, string>;
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const pending = pendingCollections.get(body.token);
|
|
822
|
-
if (!pending) {
|
|
823
|
-
return addCors(
|
|
824
|
-
jsonResponse({ error: "Invalid or expired collection token" }, 400),
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// One-time use
|
|
829
|
-
pendingCollections.delete(body.token);
|
|
830
|
-
|
|
831
|
-
// Check expiry (10 min)
|
|
832
|
-
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
|
|
833
|
-
return addCors(
|
|
834
|
-
jsonResponse({ error: "Collection token expired" }, 400),
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Encrypt secret values and store as refs
|
|
839
|
-
const mergedParams = { ...pending.params };
|
|
840
|
-
for (const [fieldName, value] of Object.entries(body.values)) {
|
|
841
|
-
const fieldDef = pending.fields.find((f) => f.name === fieldName);
|
|
842
|
-
if (fieldDef?.secret && secretStore) {
|
|
843
|
-
// Store encrypted, get ref
|
|
844
|
-
const ownerId = pending.auth?.callerId ?? "anonymous";
|
|
845
|
-
const secretId = await secretStore.store(value, ownerId);
|
|
846
|
-
mergedParams[fieldName] = `secret:${secretId}`;
|
|
847
|
-
} else {
|
|
848
|
-
mergedParams[fieldName] = value;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Auto-forward to the target tool
|
|
853
|
-
const callRequest = {
|
|
854
|
-
action: "execute_tool" as const,
|
|
855
|
-
path: pending.agent,
|
|
856
|
-
tool: pending.tool,
|
|
857
|
-
params: mergedParams,
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
const toolCtx = {
|
|
861
|
-
tenantId: "default",
|
|
862
|
-
agentPath: pending.agent,
|
|
863
|
-
callerId: pending.auth?.callerId ?? "anonymous",
|
|
864
|
-
callerType: pending.auth?.callerType ?? ("system" as const),
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
const result = await registry.call({
|
|
868
|
-
...callRequest,
|
|
869
|
-
context: toolCtx,
|
|
870
|
-
} as any);
|
|
871
|
-
|
|
872
|
-
return addCors(jsonResponse({ success: true, result }));
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// --- Web pages (plain HTML, served from same server) ---
|
|
877
|
-
const htmlRes = (body: string) => addCors(new Response(body, { headers: { "Content-Type": "text/html; charset=utf-8" } }));
|
|
878
|
-
const reqUrl = new URL(req.url);
|
|
879
|
-
const baseUrl = resolveBaseUrl(req, reqUrl);
|
|
880
|
-
|
|
881
|
-
const slackConfig: SlackOAuthConfig | null =
|
|
882
|
-
process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET
|
|
883
|
-
? {
|
|
884
|
-
clientId: process.env.SLACK_CLIENT_ID,
|
|
885
|
-
clientSecret: process.env.SLACK_CLIENT_SECRET,
|
|
886
|
-
redirectUri: `${baseUrl}/auth/slack/callback`,
|
|
887
|
-
}
|
|
888
|
-
: null;
|
|
889
|
-
|
|
890
|
-
// Helper: read session from cookie
|
|
891
|
-
function getSession(r: Request): Record<string, any> | null {
|
|
892
|
-
const c = r.headers.get("Cookie") || "";
|
|
893
|
-
const m = c.match(/s_session=([^;]+)/);
|
|
894
|
-
if (!m) return null;
|
|
895
|
-
try { return JSON.parse(Buffer.from(m[1], "base64url").toString()); }
|
|
896
|
-
catch { return null; }
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Helper: generate JWT from client credentials
|
|
900
|
-
async function generateMcpToken(): Promise<string> {
|
|
901
|
-
const clientRes = await registry.call({ action: "execute_tool", path: "@auth", tool: "create_client", callerType: "system", params: {
|
|
902
|
-
name: "mcp-" + Date.now(),
|
|
903
|
-
scopes: ["*"],
|
|
904
|
-
}} as any) as any;
|
|
905
|
-
const cid = clientRes?.result?.clientId;
|
|
906
|
-
const csec = clientRes?.result?.clientSecret;
|
|
907
|
-
if (!cid || !csec) throw new Error("Failed to create client: " + JSON.stringify(clientRes));
|
|
908
|
-
|
|
909
|
-
const tokenRes = await globalThis.fetch(`http://localhost:${port}/oauth/token`, {
|
|
910
|
-
method: "POST",
|
|
911
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
912
|
-
body: new URLSearchParams({ grant_type: "client_credentials", client_id: cid, client_secret: csec }),
|
|
913
|
-
});
|
|
914
|
-
const tokenData = await tokenRes.json() as any;
|
|
915
|
-
if (!tokenData.access_token) throw new Error("Failed to get JWT: " + JSON.stringify(tokenData));
|
|
916
|
-
return tokenData.access_token;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Helper: set session cookie and redirect
|
|
920
|
-
function sessionRedirect(location: string, session: Record<string, any>): Response {
|
|
921
|
-
const data = Buffer.from(JSON.stringify(session)).toString("base64url");
|
|
922
|
-
return new Response(null, {
|
|
923
|
-
status: 302,
|
|
924
|
-
headers: {
|
|
925
|
-
Location: location,
|
|
926
|
-
"Set-Cookie": `s_session=${data}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,
|
|
927
|
-
},
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// GET / — login page (or redirect to dashboard if session exists)
|
|
932
|
-
if (path === "/" && req.method === "GET") {
|
|
933
|
-
const session = getSession(req);
|
|
934
|
-
if (session?.token) return Response.redirect(`${baseUrl}/dashboard`, 302);
|
|
935
|
-
return htmlRes(renderLoginPage(baseUrl, !!slackConfig));
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// GET /auth/slack — start Slack OAuth
|
|
939
|
-
if (path === "/auth/slack" && req.method === "GET") {
|
|
940
|
-
if (!slackConfig) return htmlRes("<h1>Slack OAuth not configured</h1>");
|
|
941
|
-
return Response.redirect(slackAuthUrl(slackConfig), 302);
|
|
640
|
+
// ── POST / → MCP JSON-RPC ──
|
|
641
|
+
if (path === "/" && req.method === "POST") {
|
|
642
|
+
const body = (await req.json()) as JsonRpcRequest;
|
|
643
|
+
const result = await handleJsonRpc(body, effectiveAuth);
|
|
644
|
+
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
942
645
|
}
|
|
943
646
|
|
|
944
|
-
//
|
|
945
|
-
if (path === "/
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const authError = reqUrl.searchParams.get("error");
|
|
949
|
-
if (authError || !authCode) return Response.redirect(`${baseUrl}/?error=${authError || "no_code"}`, 302);
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
const tokens = await exchangeSlackCode(authCode, slackConfig);
|
|
953
|
-
const profile = await getSlackProfile(tokens.access_token);
|
|
954
|
-
const teamId = profile["https://slack.com/team_id"] || "";
|
|
955
|
-
const teamName = profile["https://slack.com/team_name"] || "";
|
|
956
|
-
|
|
957
|
-
// Check if user already exists
|
|
958
|
-
console.log("[auth] Looking up slack user:", profile.sub, profile.email);
|
|
959
|
-
const existing = await registry.call({
|
|
960
|
-
action: "execute_tool", path: "@users", callerType: "system", tool: "resolve_identity",
|
|
961
|
-
params: { provider: "slack", providerUserId: profile.sub },
|
|
962
|
-
} as any) as any;
|
|
963
|
-
console.log("[auth] resolve_identity:", JSON.stringify(existing));
|
|
964
|
-
|
|
965
|
-
if (existing?.result?.found && existing?.result?.user?.tenantId) {
|
|
966
|
-
// Returning user — generate token and go to dashboard
|
|
967
|
-
console.log("[auth] Returning user, generating token...");
|
|
968
|
-
const mcpToken = await generateMcpToken();
|
|
969
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
970
|
-
userId: existing.result.user.id,
|
|
971
|
-
tenantId: existing.result.user.tenantId,
|
|
972
|
-
email: existing.result.user.email,
|
|
973
|
-
name: existing.result.user.name,
|
|
974
|
-
token: mcpToken,
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// Check if Slack team already has a tenant
|
|
979
|
-
if (teamId) {
|
|
980
|
-
console.log("[auth] Checking tenant_identities for team:", teamId);
|
|
981
|
-
try {
|
|
982
|
-
// Direct DB query via the auth store's underlying connection
|
|
983
|
-
// We'll use a simple fetch to our own MCP endpoint to call @db-connections
|
|
984
|
-
// Actually, simpler: just query the DB directly via the server's context
|
|
985
|
-
// For now, use registry.call to a custom tool or direct SQL
|
|
986
|
-
// Simplest: call @auth list_tenants and check metadata
|
|
987
|
-
// Even simpler: direct SQL via globalThis.fetch to ourselves
|
|
988
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
989
|
-
if (dbUrl) {
|
|
990
|
-
const { default: postgres } = await import("postgres");
|
|
991
|
-
const sql = postgres(dbUrl);
|
|
992
|
-
const rows = await sql`SELECT tenant_id FROM tenant_identities WHERE provider = 'slack' AND provider_org_id = ${teamId} LIMIT 1`;
|
|
993
|
-
await sql.end();
|
|
994
|
-
if (rows.length > 0) {
|
|
995
|
-
const existingTenantId = rows[0].tenant_id;
|
|
996
|
-
console.log("[auth] Found existing tenant for team:", existingTenantId);
|
|
997
|
-
|
|
998
|
-
// Create user on existing tenant
|
|
999
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: {
|
|
1000
|
-
email: profile.email, name: profile.name, tenantId: existingTenantId,
|
|
1001
|
-
}} as any) as any;
|
|
1002
|
-
const newUserId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
1003
|
-
console.log("[auth] Created user on existing tenant:", newUserId);
|
|
1004
|
-
|
|
1005
|
-
// Link identity
|
|
1006
|
-
if (newUserId) {
|
|
1007
|
-
await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
1008
|
-
userId: newUserId, provider: "slack", providerUserId: profile.sub,
|
|
1009
|
-
email: profile.email, name: profile.name,
|
|
1010
|
-
metadata: { slackTeamId: teamId, slackTeamName: teamName },
|
|
1011
|
-
}} as any);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Generate token and go to dashboard
|
|
1015
|
-
const mcpToken = await generateMcpToken();
|
|
1016
|
-
return sessionRedirect(`${baseUrl}/dashboard`, {
|
|
1017
|
-
userId: newUserId, tenantId: existingTenantId,
|
|
1018
|
-
email: profile.email, name: profile.name, token: mcpToken,
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
} catch (e: any) {
|
|
1023
|
-
console.error("[auth] tenant_identity lookup error:", e.message);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// New user — redirect to setup
|
|
1028
|
-
return sessionRedirect(`${baseUrl}/setup`, {
|
|
1029
|
-
email: profile.email,
|
|
1030
|
-
name: profile.name,
|
|
1031
|
-
picture: profile.picture,
|
|
1032
|
-
slackUserId: profile.sub,
|
|
1033
|
-
slackTeamId: teamId,
|
|
1034
|
-
slackTeamName: teamName,
|
|
1035
|
-
});
|
|
1036
|
-
} catch (err: any) {
|
|
1037
|
-
console.error("[auth] callback error:", err);
|
|
1038
|
-
return Response.redirect(`${baseUrl}/?error=oauth_failed`, 302);
|
|
1039
|
-
}
|
|
647
|
+
// ── POST /oauth/token → OAuth2 client_credentials ──
|
|
648
|
+
if (path === "/oauth/token" && req.method === "POST") {
|
|
649
|
+
const res = await handleOAuthToken(req);
|
|
650
|
+
return cors ? addCors(res) : res;
|
|
1040
651
|
}
|
|
1041
652
|
|
|
1042
|
-
// GET /
|
|
1043
|
-
if (path === "/
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
return htmlRes(renderTenantPage(baseUrl, session.email, session.name || ""));
|
|
653
|
+
// ── GET /health → Health check ──
|
|
654
|
+
if (path === "/health" && req.method === "GET") {
|
|
655
|
+
const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
|
|
656
|
+
return cors ? addCors(res) : res;
|
|
1047
657
|
}
|
|
1048
658
|
|
|
1049
|
-
//
|
|
1050
|
-
if (path === "/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
// 1. Create tenant
|
|
1057
|
-
const tenantRes = await registry.call({ action: "execute_tool", path: "@auth", callerType: "system", tool: "create_tenant", params: { name: body.tenant } } as any) as any;
|
|
1058
|
-
const tenantId = tenantRes?.result?.tenantId;
|
|
1059
|
-
if (!tenantId) return addCors(jsonResponse({ error: "Failed to create tenant" }, 400));
|
|
1060
|
-
console.log("[setup] tenant created:", tenantId);
|
|
1061
|
-
|
|
1062
|
-
// 2. Create user
|
|
1063
|
-
const userRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "create_user", params: { email: body.email, name: session?.name, tenantId } } as any) as any;
|
|
1064
|
-
const userId = userRes?.result?.id || userRes?.result?.user?.id;
|
|
1065
|
-
console.log("[setup] user created:", userId);
|
|
1066
|
-
|
|
1067
|
-
// 2b. Link tenant to Slack team
|
|
1068
|
-
if (session?.slackTeamId) {
|
|
1069
|
-
try {
|
|
1070
|
-
const dbUrl = process.env.DATABASE_URL;
|
|
1071
|
-
if (dbUrl) {
|
|
1072
|
-
const { default: postgres } = await import("postgres");
|
|
1073
|
-
const sql = postgres(dbUrl);
|
|
1074
|
-
const id = "ti_" + Math.random().toString(36).slice(2, 14);
|
|
1075
|
-
await sql`INSERT INTO tenant_identities (id, tenant_id, provider, provider_org_id, name) VALUES (${id}, ${tenantId}, 'slack', ${session.slackTeamId}, ${session.slackTeamName || ''})`;
|
|
1076
|
-
await sql.end();
|
|
1077
|
-
console.log("[setup] Created tenant_identity for slack team:", session.slackTeamId);
|
|
1078
|
-
}
|
|
1079
|
-
} catch (e: any) { console.error("[setup] tenant_identity insert error:", e.message); }
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// 3. Link Slack identity
|
|
1083
|
-
if (session?.slackUserId && userId) {
|
|
1084
|
-
console.log("[setup] linking slack identity:", session.slackUserId);
|
|
1085
|
-
const linkRes = await registry.call({ action: "execute_tool", path: "@users", callerType: "system", tool: "link_identity", params: {
|
|
1086
|
-
userId,
|
|
1087
|
-
provider: "slack",
|
|
1088
|
-
providerUserId: session.slackUserId,
|
|
1089
|
-
email: body.email,
|
|
1090
|
-
name: session.name,
|
|
1091
|
-
metadata:{ slackTeamId: session.slackTeamId, slackTeamName: session.slackTeamName }, callerType: "system" }} as any);
|
|
1092
|
-
console.log("[setup] link_identity result:", JSON.stringify(linkRes));
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// 4. Generate MCP token
|
|
1096
|
-
const mcpToken = await generateMcpToken();
|
|
1097
|
-
console.log("[setup] token generated, length:", mcpToken.length);
|
|
1098
|
-
|
|
1099
|
-
return addCors(jsonResponse({ success: true, result: { tenantId, userId, token: mcpToken } }));
|
|
1100
|
-
} catch (err: any) {
|
|
1101
|
-
console.error("[setup] error:", err);
|
|
1102
|
-
return addCors(jsonResponse({ error: err.message }, 400));
|
|
1103
|
-
}
|
|
659
|
+
// ── GET /.well-known/jwks.json → JWKS public keys ──
|
|
660
|
+
if (path === "/.well-known/jwks.json" && req.method === "GET") {
|
|
661
|
+
const jwks = serverSigningKeys.length > 0
|
|
662
|
+
? await buildJwks(serverSigningKeys)
|
|
663
|
+
: { keys: [] };
|
|
664
|
+
const res = jsonResponse(jwks);
|
|
665
|
+
return cors ? addCors(res) : res;
|
|
1104
666
|
}
|
|
1105
667
|
|
|
1106
|
-
//
|
|
1107
|
-
if (path === "/
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
668
|
+
// ── GET /.well-known/configuration → Server discovery ──
|
|
669
|
+
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
670
|
+
const baseUrl = new URL(req.url).origin;
|
|
671
|
+
const res = jsonResponse({
|
|
672
|
+
issuer: baseUrl,
|
|
673
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
674
|
+
token_endpoint: `${baseUrl}/oauth/token`,
|
|
675
|
+
agents_endpoint: `${baseUrl}/list`,
|
|
676
|
+
call_endpoint: baseUrl,
|
|
677
|
+
supported_grant_types: ["client_credentials"],
|
|
678
|
+
agents: registry.listPaths(),
|
|
1114
679
|
});
|
|
680
|
+
return cors ? addCors(res) : res;
|
|
1115
681
|
}
|
|
1116
682
|
|
|
1117
|
-
// GET /
|
|
1118
|
-
if (path === "/
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
683
|
+
// ── GET /list → List agents (legacy endpoint) ──
|
|
684
|
+
if (path === "/list" && req.method === "GET") {
|
|
685
|
+
const agents = registry.list();
|
|
686
|
+
const visible = agents.filter((agent) => canSeeAgent(agent, effectiveAuth));
|
|
687
|
+
const res = jsonResponse(
|
|
688
|
+
visible.map((agent) => ({
|
|
689
|
+
path: agent.path,
|
|
690
|
+
name: agent.config?.name,
|
|
691
|
+
description: agent.config?.description,
|
|
692
|
+
supportedActions: agent.config?.supportedActions,
|
|
693
|
+
integration: agent.config?.integration || null,
|
|
694
|
+
tools: agent.tools
|
|
695
|
+
.filter((t) => {
|
|
696
|
+
const tv = t.visibility ?? "internal";
|
|
697
|
+
if (effectiveAuth?.isRoot) return true;
|
|
698
|
+
if (tv === "public") return true;
|
|
699
|
+
if (tv === "internal" && effectiveAuth) return true;
|
|
700
|
+
return false;
|
|
701
|
+
})
|
|
702
|
+
.map((t) => ({
|
|
703
|
+
name: t.name,
|
|
704
|
+
description: t.description,
|
|
705
|
+
})),
|
|
706
|
+
})),
|
|
707
|
+
);
|
|
708
|
+
return cors ? addCors(res) : res;
|
|
1131
709
|
}
|
|
1132
710
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
},
|
|
711
|
+
// ── Not found ──
|
|
712
|
+
const res = jsonResponse(
|
|
713
|
+
{
|
|
714
|
+
jsonrpc: "2.0",
|
|
715
|
+
id: null,
|
|
716
|
+
error: {
|
|
717
|
+
code: -32601,
|
|
718
|
+
message: `Not found: ${req.method} ${path}`,
|
|
1142
719
|
},
|
|
1143
|
-
|
|
1144
|
-
|
|
720
|
+
},
|
|
721
|
+
404,
|
|
1145
722
|
);
|
|
723
|
+
return cors ? addCors(res) : res;
|
|
1146
724
|
} catch (err) {
|
|
1147
725
|
console.error("[server] Request error:", err);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
726
|
+
const res = jsonResponse(
|
|
727
|
+
{
|
|
728
|
+
jsonrpc: "2.0",
|
|
729
|
+
id: null,
|
|
730
|
+
error: {
|
|
731
|
+
code: -32603,
|
|
732
|
+
message: err instanceof Error ? err.message : "Internal error",
|
|
1154
733
|
},
|
|
1155
|
-
|
|
1156
|
-
|
|
734
|
+
},
|
|
735
|
+
500,
|
|
1157
736
|
);
|
|
737
|
+
return cors ? addCors(res) : res;
|
|
1158
738
|
}
|
|
1159
739
|
}
|
|
1160
740
|
|
|
@@ -1162,38 +742,45 @@ export function createAgentServer(
|
|
|
1162
742
|
// Server lifecycle
|
|
1163
743
|
// ──────────────────────────────────────────
|
|
1164
744
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
745
|
+
return {
|
|
746
|
+
url: null,
|
|
747
|
+
registry,
|
|
748
|
+
|
|
749
|
+
async start() {
|
|
750
|
+
// Load or generate signing key for JWKS
|
|
751
|
+
if (options.signingKey) {
|
|
752
|
+
serverSigningKeys.push(options.signingKey);
|
|
753
|
+
} else if (authConfig?.store?.getSigningKeys) {
|
|
754
|
+
const stored = await authConfig.store.getSigningKeys() ?? [];
|
|
755
|
+
for (const exported of stored) {
|
|
756
|
+
serverSigningKeys.push(await importSigningKey(exported));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (serverSigningKeys.length === 0) {
|
|
760
|
+
const key = await generateSigningKey();
|
|
761
|
+
serverSigningKeys.push(key);
|
|
762
|
+
if (authConfig?.store?.storeSigningKey) {
|
|
763
|
+
await authConfig.store.storeSigningKey(await exportSigningKey(key));
|
|
764
|
+
}
|
|
1179
765
|
}
|
|
1180
|
-
|
|
766
|
+
|
|
767
|
+
serverInstance = Bun.serve({
|
|
768
|
+
port,
|
|
769
|
+
hostname,
|
|
770
|
+
fetch,
|
|
771
|
+
});
|
|
772
|
+
(this as any).url = `http://${hostname}:${port}`;
|
|
773
|
+
console.log(`[agents-sdk] Server listening on http://${hostname}:${port}`);
|
|
1181
774
|
},
|
|
1182
775
|
|
|
1183
|
-
async stop()
|
|
776
|
+
async stop() {
|
|
1184
777
|
if (serverInstance) {
|
|
1185
778
|
serverInstance.stop();
|
|
1186
779
|
serverInstance = null;
|
|
1187
|
-
|
|
780
|
+
(this as any).url = null;
|
|
1188
781
|
}
|
|
1189
782
|
},
|
|
1190
783
|
|
|
1191
784
|
fetch,
|
|
1192
|
-
|
|
1193
|
-
get url(): string | null {
|
|
1194
|
-
return serverUrl;
|
|
1195
|
-
},
|
|
1196
785
|
};
|
|
1197
|
-
|
|
1198
|
-
return server;
|
|
1199
786
|
}
|