@frankieai/server 0.1.5 → 0.2.1
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/chunk-UZSHFMJN.js +800 -0
- package/dist/chunk-UZSHFMJN.js.map +1 -0
- package/dist/{chunk-K75HPI2W.js → chunk-WFENEYOV.js} +622 -341
- package/dist/chunk-WFENEYOV.js.map +1 -0
- package/dist/{chunk-2BTCA3F3.js → chunk-XCIM765D.js} +40 -101
- package/dist/chunk-XCIM765D.js.map +1 -0
- package/dist/control-server.js +2 -2
- package/dist/index.d.ts +49 -2
- package/dist/index.js +7 -7
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-2BTCA3F3.js.map +0 -1
- package/dist/chunk-3J66REZR.js +0 -458
- package/dist/chunk-3J66REZR.js.map +0 -1
- package/dist/chunk-K75HPI2W.js.map +0 -1
package/dist/chunk-3J66REZR.js
DELETED
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
connectMcpTools,
|
|
3
|
-
inspectApiCapabilities,
|
|
4
|
-
safeEqual,
|
|
5
|
-
toPublicTenant
|
|
6
|
-
} from "./chunk-K75HPI2W.js";
|
|
7
|
-
|
|
8
|
-
// src/control/app.ts
|
|
9
|
-
import { randomBytes } from "crypto";
|
|
10
|
-
import { Hono } from "hono";
|
|
11
|
-
import { parseFrankieConfig } from "@frankieai/core";
|
|
12
|
-
function createControlApp(options) {
|
|
13
|
-
const { store, sessions } = options;
|
|
14
|
-
const adminToken = options.adminToken ?? process.env["FRANKIE_ADMIN_TOKEN"];
|
|
15
|
-
const log = options.logger ?? ((event) => console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event })));
|
|
16
|
-
const app = new Hono();
|
|
17
|
-
app.use("/admin/*", async (c, next) => {
|
|
18
|
-
if (!adminToken) {
|
|
19
|
-
return c.json({ error: "Control plane is locked: set FRANKIE_ADMIN_TOKEN." }, 503);
|
|
20
|
-
}
|
|
21
|
-
const bearer = c.req.header("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
|
|
22
|
-
if (!safeEqual(bearer, adminToken)) {
|
|
23
|
-
return c.json({ error: "Unauthorized." }, 401);
|
|
24
|
-
}
|
|
25
|
-
await next();
|
|
26
|
-
return;
|
|
27
|
-
});
|
|
28
|
-
const ownerOf = (c) => c.req.header("x-frankie-owner") || void 0;
|
|
29
|
-
const loginOf = (c) => c.req.header("x-frankie-login")?.toLowerCase() || void 0;
|
|
30
|
-
const accessibleOrgs = async (c) => {
|
|
31
|
-
const owner = ownerOf(c);
|
|
32
|
-
const login = loginOf(c);
|
|
33
|
-
if (!owner && !login) return void 0;
|
|
34
|
-
const orgs = /* @__PURE__ */ new Set();
|
|
35
|
-
if (owner) orgs.add(owner);
|
|
36
|
-
if (login) for (const m of await store.membershipsForLogin(login)) orgs.add(m.org);
|
|
37
|
-
return orgs;
|
|
38
|
-
};
|
|
39
|
-
const canManageMembers = async (c, org) => {
|
|
40
|
-
const owner = ownerOf(c);
|
|
41
|
-
const login = loginOf(c);
|
|
42
|
-
if (!owner && !login) return true;
|
|
43
|
-
if (owner && owner === org) return true;
|
|
44
|
-
if (login) {
|
|
45
|
-
const memberships = await store.membershipsForLogin(login);
|
|
46
|
-
if (memberships.some((m) => m.org === org && m.role === "admin")) return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
};
|
|
50
|
-
app.post("/admin/tenants", async (c) => {
|
|
51
|
-
const body = await readJson(c) ?? {};
|
|
52
|
-
if (!body.name?.trim()) return c.json({ error: "`name` is required." }, 400);
|
|
53
|
-
const ownerId = ownerOf(c);
|
|
54
|
-
let id = slug(body.name);
|
|
55
|
-
if (await store.get(id)) id = `${id}-${randomBytes(3).toString("hex")}`;
|
|
56
|
-
const publishableKey = `pk_live_${randomBytes(12).toString("hex")}`;
|
|
57
|
-
const jwtSecret = randomBytes(36).toString("base64");
|
|
58
|
-
const sessionSecret = randomBytes(36).toString("base64");
|
|
59
|
-
let config;
|
|
60
|
-
try {
|
|
61
|
-
config = parseFrankieConfig({
|
|
62
|
-
tenantId: id,
|
|
63
|
-
branding: { name: body.name },
|
|
64
|
-
agent: {
|
|
65
|
-
...body.goal ? { systemPrompt: body.goal } : {},
|
|
66
|
-
...body.greeting ? { greeting: body.greeting } : {}
|
|
67
|
-
},
|
|
68
|
-
auth: {
|
|
69
|
-
publishableKey,
|
|
70
|
-
allowedOrigins: body.allowedOrigins ?? [],
|
|
71
|
-
...body.allowAnonymous ? { allowAnonymous: true } : {}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
} catch (error) {
|
|
75
|
-
return c.json({ error: `Invalid tenant: ${error.message}` }, 400);
|
|
76
|
-
}
|
|
77
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
78
|
-
const record = {
|
|
79
|
-
id,
|
|
80
|
-
...ownerId ? { ownerId } : {},
|
|
81
|
-
config,
|
|
82
|
-
jwtSecret,
|
|
83
|
-
sessionSecret,
|
|
84
|
-
createdAt: now,
|
|
85
|
-
updatedAt: now
|
|
86
|
-
};
|
|
87
|
-
await store.put(record);
|
|
88
|
-
log({ level: "info", event: "tenant_created", tenant: id, ...ownerId ? { owner: ownerId } : {} });
|
|
89
|
-
return c.json(
|
|
90
|
-
{
|
|
91
|
-
tenant: toPublicTenant(record),
|
|
92
|
-
secrets: { publishableKey, jwtSecret },
|
|
93
|
-
next: "Set FRANKIE_JWT_SECRET to the jwtSecret on your backend, then run `frankie init`."
|
|
94
|
-
},
|
|
95
|
-
201
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
const own = async (c) => {
|
|
99
|
-
const id = c.req.param("id");
|
|
100
|
-
const record = id ? await store.get(id) : null;
|
|
101
|
-
if (!record) return null;
|
|
102
|
-
const orgs = await accessibleOrgs(c);
|
|
103
|
-
if (!orgs) return record;
|
|
104
|
-
if (!record.ownerId) return record;
|
|
105
|
-
return orgs.has(record.ownerId) ? record : null;
|
|
106
|
-
};
|
|
107
|
-
app.get("/admin/tenants", async (c) => {
|
|
108
|
-
const orgs = await accessibleOrgs(c);
|
|
109
|
-
if (!orgs) return c.json({ tenants: (await store.list()).map(toPublicTenant) });
|
|
110
|
-
const records = [];
|
|
111
|
-
for (const org of orgs) records.push(...await store.list(org));
|
|
112
|
-
return c.json({ tenants: records.map(toPublicTenant) });
|
|
113
|
-
});
|
|
114
|
-
const LOGIN_RE = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i;
|
|
115
|
-
app.get("/admin/orgs/:org/members", async (c) => {
|
|
116
|
-
const org = decodeURIComponent(c.req.param("org"));
|
|
117
|
-
if (!await canManageMembers(c, org)) return c.json({ error: "Forbidden." }, 403);
|
|
118
|
-
return c.json({ members: await store.listMembers(org) });
|
|
119
|
-
});
|
|
120
|
-
app.post("/admin/orgs/:org/members", async (c) => {
|
|
121
|
-
const org = decodeURIComponent(c.req.param("org"));
|
|
122
|
-
if (!await canManageMembers(c, org)) return c.json({ error: "Forbidden." }, 403);
|
|
123
|
-
const body = await readJson(c) ?? {};
|
|
124
|
-
const login = String(body.login ?? "").trim().toLowerCase();
|
|
125
|
-
if (!LOGIN_RE.test(login)) return c.json({ error: "Enter a valid GitHub username." }, 400);
|
|
126
|
-
const role = body.role === "admin" ? "admin" : "member";
|
|
127
|
-
await store.putMember({ org, login, role, invitedBy: ownerOf(c) ?? loginOf(c) ?? "", createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
128
|
-
log({ level: "info", event: "member_invited", org, login, role });
|
|
129
|
-
return c.json({ members: await store.listMembers(org) }, 201);
|
|
130
|
-
});
|
|
131
|
-
app.patch("/admin/orgs/:org/members/:login", async (c) => {
|
|
132
|
-
const org = decodeURIComponent(c.req.param("org"));
|
|
133
|
-
if (!await canManageMembers(c, org)) return c.json({ error: "Forbidden." }, 403);
|
|
134
|
-
const login = decodeURIComponent(c.req.param("login")).toLowerCase();
|
|
135
|
-
const existing = (await store.listMembers(org)).find((m) => m.login === login);
|
|
136
|
-
if (!existing) return c.json({ error: "No such member." }, 404);
|
|
137
|
-
const body = await readJson(c) ?? {};
|
|
138
|
-
const role = body.role === "admin" ? "admin" : "member";
|
|
139
|
-
await store.putMember({ ...existing, role });
|
|
140
|
-
return c.json({ members: await store.listMembers(org) });
|
|
141
|
-
});
|
|
142
|
-
app.delete("/admin/orgs/:org/members/:login", async (c) => {
|
|
143
|
-
const org = decodeURIComponent(c.req.param("org"));
|
|
144
|
-
if (!await canManageMembers(c, org)) return c.json({ error: "Forbidden." }, 403);
|
|
145
|
-
const login = decodeURIComponent(c.req.param("login")).toLowerCase();
|
|
146
|
-
await store.deleteMember(org, login);
|
|
147
|
-
log({ level: "info", event: "member_removed", org, login });
|
|
148
|
-
return c.json({ members: await store.listMembers(org) });
|
|
149
|
-
});
|
|
150
|
-
app.get("/admin/my-orgs", async (c) => {
|
|
151
|
-
const login = loginOf(c);
|
|
152
|
-
const memberships = login ? await store.membershipsForLogin(login) : [];
|
|
153
|
-
return c.json({ memberships });
|
|
154
|
-
});
|
|
155
|
-
app.get("/admin/tenants/:id/conversations", async (c) => {
|
|
156
|
-
const record = await own(c);
|
|
157
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
158
|
-
const conversations = sessions ? await sessions.listByTenant(record.id) : [];
|
|
159
|
-
return c.json({ conversations: conversations.map(publicConversationSummary) });
|
|
160
|
-
});
|
|
161
|
-
app.get("/admin/tenants/:id/conversations/:conversationId", async (c) => {
|
|
162
|
-
const record = await own(c);
|
|
163
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
164
|
-
const conversation = sessions ? await sessions.get(c.req.param("conversationId")) : void 0;
|
|
165
|
-
if (!conversation || conversation.tenantId !== record.id) return c.json({ error: "No such conversation." }, 404);
|
|
166
|
-
return c.json({ conversation: publicConversation(conversation) });
|
|
167
|
-
});
|
|
168
|
-
app.get("/admin/tenants/:id", async (c) => {
|
|
169
|
-
const record = await own(c);
|
|
170
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
171
|
-
return c.json({ tenant: toPublicTenant(record) });
|
|
172
|
-
});
|
|
173
|
-
app.get("/admin/tenants/:id/jwt-secret", async (c) => {
|
|
174
|
-
const record = await own(c);
|
|
175
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
176
|
-
if (!record.jwtSecret) return c.json({ error: "This workspace has no JWT secret." }, 404);
|
|
177
|
-
return c.json({ secrets: { jwtSecret: record.jwtSecret } });
|
|
178
|
-
});
|
|
179
|
-
app.delete("/admin/tenants/:id", async (c) => {
|
|
180
|
-
const record = await own(c);
|
|
181
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
182
|
-
if (record.locked) {
|
|
183
|
-
return c.json({ error: "This workspace is locked. Unlock it before deleting." }, 409);
|
|
184
|
-
}
|
|
185
|
-
await store.delete(record.id);
|
|
186
|
-
log({ level: "info", event: "tenant_deleted", tenant: record.id });
|
|
187
|
-
return c.json({ ok: true, id: record.id });
|
|
188
|
-
});
|
|
189
|
-
app.put("/admin/tenants/:id/lock", async (c) => {
|
|
190
|
-
const record = await own(c);
|
|
191
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
192
|
-
const body = await readJson(c) ?? {};
|
|
193
|
-
const updated = { ...record, locked: Boolean(body.locked), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
194
|
-
await store.put(updated);
|
|
195
|
-
log({ level: "info", event: "tenant_lock_set", tenant: record.id, locked: updated.locked });
|
|
196
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
197
|
-
});
|
|
198
|
-
app.put("/admin/tenants/:id/repository-install", async (c) => {
|
|
199
|
-
const record = await own(c);
|
|
200
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
201
|
-
const body = await readJson(c) ?? {};
|
|
202
|
-
const repository = String(body.repository ?? "").trim();
|
|
203
|
-
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
|
|
204
|
-
return c.json({ error: "Enter a valid GitHub repository." }, 400);
|
|
205
|
-
}
|
|
206
|
-
const status = body.status === "submitted" || body.status === "failed" ? body.status : "installing";
|
|
207
|
-
const pullRequestUrl = typeof body.pullRequestUrl === "string" && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+\/?$/.test(body.pullRequestUrl) ? body.pullRequestUrl : void 0;
|
|
208
|
-
const updated = {
|
|
209
|
-
...record,
|
|
210
|
-
repositoryInstall: {
|
|
211
|
-
repository,
|
|
212
|
-
status,
|
|
213
|
-
...typeof body.runId === "string" && body.runId ? { runId: body.runId } : {},
|
|
214
|
-
...pullRequestUrl ? { pullRequestUrl } : {},
|
|
215
|
-
requiresJwtSecret: Boolean(body.requiresJwtSecret),
|
|
216
|
-
...typeof body.failureMessage === "string" && body.failureMessage ? { failureMessage: body.failureMessage } : {},
|
|
217
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
218
|
-
},
|
|
219
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
220
|
-
};
|
|
221
|
-
await store.put(updated);
|
|
222
|
-
log({ level: "info", event: "tenant_repository_install_updated", tenant: record.id, status });
|
|
223
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
224
|
-
});
|
|
225
|
-
app.put("/admin/tenants/:id/connection", async (c) => {
|
|
226
|
-
const record = await own(c);
|
|
227
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
228
|
-
const body = await readJson(c) ?? {};
|
|
229
|
-
let config;
|
|
230
|
-
try {
|
|
231
|
-
config = parseFrankieConfig({
|
|
232
|
-
...record.config,
|
|
233
|
-
mcpServers: body.mcpServers === void 0 ? record.config.mcpServers : mergeSources(record.config.mcpServers, body.mcpServers),
|
|
234
|
-
apis: body.apis === void 0 ? record.config.apis : mergeSources(record.config.apis, body.apis)
|
|
235
|
-
});
|
|
236
|
-
} catch (error) {
|
|
237
|
-
return c.json({ error: `Invalid connection: ${error.message}` }, 400);
|
|
238
|
-
}
|
|
239
|
-
const allow = new Set(config.agent.tools.allow);
|
|
240
|
-
for (const api of config.apis) allow.add(`api__${api.name}`);
|
|
241
|
-
for (const server of config.mcpServers) allow.add(`mcp__${server.name}`);
|
|
242
|
-
config = parseFrankieConfig({
|
|
243
|
-
...config,
|
|
244
|
-
agent: { ...config.agent, tools: { ...config.agent.tools, allow: [...allow] } }
|
|
245
|
-
});
|
|
246
|
-
const apisWithCapabilities = await Promise.all(
|
|
247
|
-
config.apis.map(async (api) => {
|
|
248
|
-
try {
|
|
249
|
-
const capabilities = await inspectApiCapabilities(api, {
|
|
250
|
-
policy: config.agent.tools,
|
|
251
|
-
fetchImpl: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(8e3) })
|
|
252
|
-
});
|
|
253
|
-
return { ...api, capabilities };
|
|
254
|
-
} catch (error) {
|
|
255
|
-
log({ level: "warn", event: "api_capability_inspection_failed", tenant: record.id, api: api.name, error: error instanceof Error ? error.message : String(error) });
|
|
256
|
-
return { ...api, capabilities: [] };
|
|
257
|
-
}
|
|
258
|
-
})
|
|
259
|
-
);
|
|
260
|
-
config = parseFrankieConfig({ ...config, apis: apisWithCapabilities });
|
|
261
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
262
|
-
await store.put(updated);
|
|
263
|
-
log({ level: "info", event: "tenant_connection_updated", tenant: record.id });
|
|
264
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
265
|
-
});
|
|
266
|
-
app.put("/admin/tenants/:id/tool-selection", async (c) => {
|
|
267
|
-
const record = await own(c);
|
|
268
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
269
|
-
const body = await readJson(c) ?? {};
|
|
270
|
-
const requested = Array.isArray(body.sourceIds) ? body.sourceIds.filter((id) => typeof id === "string") : [];
|
|
271
|
-
const sources = [
|
|
272
|
-
...record.config.mcpServers.map((source) => ({ id: "mcp:" + source.name, prefix: "mcp__" + source.name })),
|
|
273
|
-
...record.config.apis.map((source) => ({ id: "openapi:" + source.name, prefix: "api__" + source.name }))
|
|
274
|
-
];
|
|
275
|
-
const known = new Map(sources.map((source) => [source.id, source]));
|
|
276
|
-
if (requested.some((id) => !known.has(id))) return c.json({ error: "One of the selected tool sources no longer exists." }, 400);
|
|
277
|
-
const sourcePrefixes = new Set(sources.map((source) => source.prefix));
|
|
278
|
-
const otherAllow = record.config.agent.tools.allow.filter((entry) => ![...sourcePrefixes].some((prefix) => entry === prefix || entry.startsWith(prefix + "__")));
|
|
279
|
-
const selectedAllow = requested.map((id) => known.get(id).prefix);
|
|
280
|
-
const config = parseFrankieConfig({
|
|
281
|
-
...record.config,
|
|
282
|
-
agent: { ...record.config.agent, tools: { ...record.config.agent.tools, allow: [...otherAllow, ...selectedAllow] } },
|
|
283
|
-
apis: record.config.apis.map((s) => ({ ...s, enabled: requested.includes("openapi:" + s.name) })),
|
|
284
|
-
mcpServers: record.config.mcpServers.map((s) => ({ ...s, enabled: requested.includes("mcp:" + s.name) }))
|
|
285
|
-
});
|
|
286
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
287
|
-
await store.put(updated);
|
|
288
|
-
log({ level: "info", event: "tenant_tool_selection_updated", tenant: record.id, sources: requested });
|
|
289
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
290
|
-
});
|
|
291
|
-
app.post("/admin/tenants/:id/tool-discovery", async (c) => {
|
|
292
|
-
const record = await own(c);
|
|
293
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
294
|
-
const body = await readJson(c);
|
|
295
|
-
const source = record.config.mcpServers.find((s) => s.name === body?.name);
|
|
296
|
-
if (!source) return c.json({ error: "MCP source not found." }, 404);
|
|
297
|
-
const session = {
|
|
298
|
-
id: "inventory",
|
|
299
|
-
tenantId: record.id,
|
|
300
|
-
user: { id: "inventory", anonymous: true, claims: {} },
|
|
301
|
-
context: {},
|
|
302
|
-
messages: [],
|
|
303
|
-
createdAt: Date.now(),
|
|
304
|
-
lastActiveAt: Date.now(),
|
|
305
|
-
messageCount: 0,
|
|
306
|
-
costUsd: 0,
|
|
307
|
-
busy: false
|
|
308
|
-
};
|
|
309
|
-
const result = await connectMcpTools({
|
|
310
|
-
servers: [{ ...source, enabled: true, selectedTools: void 0 }],
|
|
311
|
-
session,
|
|
312
|
-
policy: { allow: ["mcp__" + source.name], deny: [], denyByDefault: true },
|
|
313
|
-
connectTimeoutMs: 8e3
|
|
314
|
-
});
|
|
315
|
-
try {
|
|
316
|
-
if (result.failed.length) return c.json({ error: "Tool discovery could not connect. If this MCP requires a signed-in user, enter tool names from its tools/list response; Frankie will validate availability when that user chats." }, 422);
|
|
317
|
-
const discoveredTools = result.tools.map((tool) => ({ name: tool.name.slice(("mcp__" + source.name + "__").length), description: tool.description }));
|
|
318
|
-
const config = parseFrankieConfig({ ...record.config, mcpServers: record.config.mcpServers.map((s) => s.name === source.name ? { ...s, discoveredTools } : s) });
|
|
319
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
320
|
-
await store.put(updated);
|
|
321
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
322
|
-
} finally {
|
|
323
|
-
await result.close();
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
app.put("/admin/tenants/:id/tool-access", async (c) => {
|
|
327
|
-
const record = await own(c);
|
|
328
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
329
|
-
const body = await readJson(c);
|
|
330
|
-
if (!body || !["openapi", "mcp"].includes(body.kind ?? "") || !Array.isArray(body.selectedTools) || !body.selectedTools.every((v) => typeof v === "string" && v.length > 0 && !v.includes("*")))
|
|
331
|
-
return c.json({ error: "Choose specific operations or tool names." }, 400);
|
|
332
|
-
const selectedTools = [...new Set(body.selectedTools)];
|
|
333
|
-
const source = body.kind === "openapi" ? record.config.apis.find((s) => s.name === body.name) : record.config.mcpServers.find((s) => s.name === body.name);
|
|
334
|
-
if (!source) return c.json({ error: "Source not found." }, 404);
|
|
335
|
-
if (body.kind === "openapi") {
|
|
336
|
-
const api = record.config.apis.find((s) => s.name === body.name);
|
|
337
|
-
if (selectedTools.some((id) => !api.capabilities.some((operation) => operation.operationId === id)))
|
|
338
|
-
return c.json({ error: "An operation changed. Refresh the source and choose again." }, 400);
|
|
339
|
-
}
|
|
340
|
-
const config = parseFrankieConfig({
|
|
341
|
-
...record.config,
|
|
342
|
-
apis: record.config.apis.map((s) => body.kind === "openapi" && s.name === body.name ? { ...s, selectedTools, ...typeof body.exposeAsMcp === "boolean" ? { exposeAsMcp: body.exposeAsMcp } : {} } : s),
|
|
343
|
-
mcpServers: record.config.mcpServers.map((s) => body.kind === "mcp" && s.name === body.name ? { ...s, selectedTools } : s)
|
|
344
|
-
});
|
|
345
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
346
|
-
await store.put(updated);
|
|
347
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
348
|
-
});
|
|
349
|
-
app.put("/admin/tenants/:id/origins", async (c) => {
|
|
350
|
-
const record = await own(c);
|
|
351
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
352
|
-
const body = await readJson(c) ?? {};
|
|
353
|
-
let config;
|
|
354
|
-
try {
|
|
355
|
-
config = parseFrankieConfig({
|
|
356
|
-
...record.config,
|
|
357
|
-
auth: { ...record.config.auth, allowedOrigins: body.allowedOrigins ?? [] }
|
|
358
|
-
});
|
|
359
|
-
} catch (error) {
|
|
360
|
-
return c.json({ error: `Invalid origins: ${error.message}` }, 400);
|
|
361
|
-
}
|
|
362
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
363
|
-
await store.put(updated);
|
|
364
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
365
|
-
});
|
|
366
|
-
app.put("/admin/tenants/:id/theme", async (c) => {
|
|
367
|
-
const record = await own(c);
|
|
368
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
369
|
-
const body = await readJson(c) ?? {};
|
|
370
|
-
let config;
|
|
371
|
-
try {
|
|
372
|
-
config = parseFrankieConfig({ ...record.config, theme: body.theme ?? {} });
|
|
373
|
-
} catch (error) {
|
|
374
|
-
return c.json({ error: `Invalid theme: ${error.message}` }, 400);
|
|
375
|
-
}
|
|
376
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
377
|
-
await store.put(updated);
|
|
378
|
-
log({ level: "info", event: "tenant_theme_updated", tenant: record.id });
|
|
379
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
380
|
-
});
|
|
381
|
-
app.put("/admin/tenants/:id/features", async (c) => {
|
|
382
|
-
const record = await own(c);
|
|
383
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
384
|
-
const body = await readJson(c) ?? {};
|
|
385
|
-
let config;
|
|
386
|
-
try {
|
|
387
|
-
config = parseFrankieConfig({ ...record.config, features: body.features ?? {} });
|
|
388
|
-
} catch (error) {
|
|
389
|
-
return c.json({ error: `Invalid features: ${error.message}` }, 400);
|
|
390
|
-
}
|
|
391
|
-
const updated = { ...record, config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
392
|
-
await store.put(updated);
|
|
393
|
-
log({ level: "info", event: "tenant_features_updated", tenant: record.id });
|
|
394
|
-
return c.json({ tenant: toPublicTenant(updated) });
|
|
395
|
-
});
|
|
396
|
-
app.post("/admin/tenants/:id/rotate-jwt-secret", async (c) => {
|
|
397
|
-
const record = await own(c);
|
|
398
|
-
if (!record) return c.json({ error: "No such tenant." }, 404);
|
|
399
|
-
const jwtSecret = randomBytes(36).toString("base64");
|
|
400
|
-
const updated = { ...record, jwtSecret, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
401
|
-
await store.put(updated);
|
|
402
|
-
log({ level: "info", event: "tenant_secret_rotated", tenant: record.id });
|
|
403
|
-
return c.json({ tenant: toPublicTenant(updated), secrets: { jwtSecret } });
|
|
404
|
-
});
|
|
405
|
-
app.onError((error, c) => {
|
|
406
|
-
const message = error instanceof Error ? error.message : "Internal error.";
|
|
407
|
-
log({ level: "error", event: "control_error", message });
|
|
408
|
-
return c.json({ error: `Control plane error: ${message}` }, 500);
|
|
409
|
-
});
|
|
410
|
-
return app;
|
|
411
|
-
}
|
|
412
|
-
function conversationTitle(session) {
|
|
413
|
-
return session.title?.trim() || "New chat";
|
|
414
|
-
}
|
|
415
|
-
function publicConversationSummary(session) {
|
|
416
|
-
return {
|
|
417
|
-
id: session.id,
|
|
418
|
-
title: conversationTitle(session),
|
|
419
|
-
createdAt: session.createdAt,
|
|
420
|
-
updatedAt: session.lastActiveAt,
|
|
421
|
-
messageCount: session.messageCount,
|
|
422
|
-
user: { id: session.user.id, ...session.user.displayName ? { displayName: session.user.displayName } : {}, anonymous: session.user.anonymous }
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
function publicConversation(session) {
|
|
426
|
-
return {
|
|
427
|
-
...publicConversationSummary(session),
|
|
428
|
-
// New UI transcripts are stored in clientMessages. Older/interrupted turns
|
|
429
|
-
// still have the durable agent transcript, so expose that instead of showing
|
|
430
|
-
// a conversation row with an empty detail page.
|
|
431
|
-
messages: Array.isArray(session.clientMessages) && session.clientMessages.length ? session.clientMessages : session.messages,
|
|
432
|
-
...session.busy ? { busy: true } : {}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
async function readJson(c) {
|
|
436
|
-
try {
|
|
437
|
-
return await c.req.json();
|
|
438
|
-
} catch {
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function slug(value) {
|
|
443
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "tenant";
|
|
444
|
-
}
|
|
445
|
-
function mergeSources(current, updates) {
|
|
446
|
-
if (!updates.length) return [];
|
|
447
|
-
const names = new Set(updates.map((value) => typeof value === "object" && value !== null ? value.name : void 0));
|
|
448
|
-
return [...current.filter((source) => !names.has(source.name)), ...updates.map((value) => {
|
|
449
|
-
if (typeof value !== "object" || value === null) return value;
|
|
450
|
-
const previous = current.find((s) => s.name === value.name);
|
|
451
|
-
return { ...previous, ...value };
|
|
452
|
-
})];
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export {
|
|
456
|
-
createControlApp
|
|
457
|
-
};
|
|
458
|
-
//# sourceMappingURL=chunk-3J66REZR.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/control/app.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport type { Context } from \"hono\";\nimport { parseFrankieConfig } from \"@frankieai/core\";\nimport { safeEqual } from \"../auth.js\";\nimport { connectMcpTools } from \"../agent/mcp.js\";\nimport { inspectApiCapabilities } from \"../agent/openapi.js\";\nimport { toPublicTenant, type ControlStore, type TenantRecord } from \"./store.js\";\nimport type { ChatSession, SessionStore } from \"../sessions.js\";\n\nexport interface ControlOptions {\n store: ControlStore;\n /** Durable session store shared with the data plane when Postgres is configured. */\n sessions?: SessionStore;\n /** Bearer token that gates the admin API. A stand-in for real user auth. */\n adminToken?: string;\n logger?: (event: Record<string, unknown>) => void;\n}\n\n/**\n * The control plane: create a tenant, hand back its keys, and manage how its\n * product connects — bring an MCP, point at an OpenAPI spec, or stay\n * knowledge-only. The dashboard and the setup CLI are thin clients over this.\n *\n * Admin auth here is a single shared token — deliberately the simplest thing\n * that is not \"nothing\", and the seam where real per-user accounts go.\n */\nexport function createControlApp(options: ControlOptions): Hono {\n const { store, sessions } = options;\n const adminToken = options.adminToken ?? process.env[\"FRANKIE_ADMIN_TOKEN\"];\n const log = options.logger ?? ((event) => console.log(JSON.stringify({ ts: new Date().toISOString(), ...event })));\n\n const app = new Hono();\n\n app.use(\"/admin/*\", async (c, next) => {\n if (!adminToken) {\n return c.json({ error: \"Control plane is locked: set FRANKIE_ADMIN_TOKEN.\" }, 503);\n }\n const bearer = c.req.header(\"authorization\")?.replace(/^Bearer\\s+/i, \"\") ?? \"\";\n if (!safeEqual(bearer, adminToken)) {\n return c.json({ error: \"Unauthorized.\" }, 401);\n }\n await next();\n return;\n });\n\n /* ------------------------------ create ------------------------------ */\n\n /** The dashboard identifies the acting user with this header (e.g. \"github:42\"). */\n const ownerOf = (c: Context): string | undefined => c.req.header(\"x-frankie-owner\") || undefined;\n /** …and their GitHub login (username), so org memberships can be resolved. */\n const loginOf = (c: Context): string | undefined => c.req.header(\"x-frankie-login\")?.toLowerCase() || undefined;\n\n /**\n * The set of org owner-ids the caller can read and manage workspaces in: their\n * own org, plus every org they've been invited to (any role). An unscoped\n * caller (no owner, no login — the raw admin token) gets undefined, meaning\n * \"all orgs\", preserving the pre-org behaviour.\n */\n const accessibleOrgs = async (c: Context): Promise<Set<string> | undefined> => {\n const owner = ownerOf(c);\n const login = loginOf(c);\n if (!owner && !login) return undefined;\n const orgs = new Set<string>();\n if (owner) orgs.add(owner);\n if (login) for (const m of await store.membershipsForLogin(login)) orgs.add(m.org);\n return orgs;\n };\n\n /** Whether the caller may manage the membership of `org` (its owner, or an admin member). */\n const canManageMembers = async (c: Context, org: string): Promise<boolean> => {\n const owner = ownerOf(c);\n const login = loginOf(c);\n if (!owner && !login) return true; // unscoped admin token\n if (owner && owner === org) return true; // the org's own owner\n if (login) {\n const memberships = await store.membershipsForLogin(login);\n if (memberships.some((m) => m.org === org && m.role === \"admin\")) return true;\n }\n return false;\n };\n\n app.post(\"/admin/tenants\", async (c) => {\n const body = (await readJson<CreateTenantBody>(c)) ?? {};\n if (!body.name?.trim()) return c.json({ error: \"`name` is required.\" }, 400);\n const ownerId = ownerOf(c);\n\n // Ids are globally unique across owners, so two customers can both have an\n // \"Acme\" without colliding: take the clean slug, or suffix it if taken.\n let id = slug(body.name);\n if (await store.get(id)) id = `${id}-${randomBytes(3).toString(\"hex\")}`;\n\n const publishableKey = `pk_live_${randomBytes(12).toString(\"hex\")}`;\n const jwtSecret = randomBytes(36).toString(\"base64\");\n const sessionSecret = randomBytes(36).toString(\"base64\");\n\n let config;\n try {\n config = parseFrankieConfig({\n tenantId: id,\n branding: { name: body.name },\n agent: {\n ...(body.goal ? { systemPrompt: body.goal } : {}),\n ...(body.greeting ? { greeting: body.greeting } : {}),\n },\n auth: {\n publishableKey,\n allowedOrigins: body.allowedOrigins ?? [],\n ...(body.allowAnonymous ? { allowAnonymous: true } : {}),\n },\n });\n } catch (error) {\n return c.json({ error: `Invalid tenant: ${(error as Error).message}` }, 400);\n }\n\n const now = new Date().toISOString();\n const record: TenantRecord = {\n id,\n ...(ownerId ? { ownerId } : {}),\n config,\n jwtSecret,\n sessionSecret,\n createdAt: now,\n updatedAt: now,\n };\n await store.put(record);\n log({ level: \"info\", event: \"tenant_created\", tenant: id, ...(ownerId ? { owner: ownerId } : {}) });\n\n // The JWT secret is shown exactly once: the customer's backend signs user\n // tokens with it, and Frankie never needs to reveal it again.\n return c.json(\n {\n tenant: toPublicTenant(record),\n secrets: { publishableKey, jwtSecret },\n next: \"Set FRANKIE_JWT_SECRET to the jwtSecret on your backend, then run `frankie init`.\",\n },\n 201,\n );\n });\n\n /* ------------------------------- read ------------------------------- */\n\n // A tenant the caller can't access (not their org, and no membership) is\n // reported as absent, never forbidden, so one owner can't probe another's ids.\n const own = async (c: Context): Promise<TenantRecord | null> => {\n const id = c.req.param(\"id\");\n const record = id ? await store.get(id) : null;\n if (!record) return null;\n const orgs = await accessibleOrgs(c);\n if (!orgs) return record; // unscoped admin token\n if (!record.ownerId) return record; // legacy ownerless tenant\n return orgs.has(record.ownerId) ? record : null;\n };\n\n app.get(\"/admin/tenants\", async (c) => {\n const orgs = await accessibleOrgs(c);\n if (!orgs) return c.json({ tenants: (await store.list()).map(toPublicTenant) });\n const records: TenantRecord[] = [];\n for (const org of orgs) records.push(...(await store.list(org)));\n return c.json({ tenants: records.map(toPublicTenant) });\n });\n\n /* ------------------------------ members ----------------------------- */\n\n const LOGIN_RE = /^[a-z\\d](?:[a-z\\d]|-(?=[a-z\\d])){0,38}$/i;\n\n app.get(\"/admin/orgs/:org/members\", async (c) => {\n const org = decodeURIComponent(c.req.param(\"org\"));\n if (!(await canManageMembers(c, org))) return c.json({ error: \"Forbidden.\" }, 403);\n return c.json({ members: await store.listMembers(org) });\n });\n\n app.post(\"/admin/orgs/:org/members\", async (c) => {\n const org = decodeURIComponent(c.req.param(\"org\"));\n if (!(await canManageMembers(c, org))) return c.json({ error: \"Forbidden.\" }, 403);\n const body = (await readJson<{ login?: string; role?: string }>(c)) ?? {};\n const login = String(body.login ?? \"\").trim().toLowerCase();\n if (!LOGIN_RE.test(login)) return c.json({ error: \"Enter a valid GitHub username.\" }, 400);\n const role = body.role === \"admin\" ? \"admin\" : \"member\";\n await store.putMember({ org, login, role, invitedBy: ownerOf(c) ?? loginOf(c) ?? \"\", createdAt: new Date().toISOString() });\n log({ level: \"info\", event: \"member_invited\", org, login, role });\n return c.json({ members: await store.listMembers(org) }, 201);\n });\n\n app.patch(\"/admin/orgs/:org/members/:login\", async (c) => {\n const org = decodeURIComponent(c.req.param(\"org\"));\n if (!(await canManageMembers(c, org))) return c.json({ error: \"Forbidden.\" }, 403);\n const login = decodeURIComponent(c.req.param(\"login\")).toLowerCase();\n const existing = (await store.listMembers(org)).find((m) => m.login === login);\n if (!existing) return c.json({ error: \"No such member.\" }, 404);\n const body = (await readJson<{ role?: string }>(c)) ?? {};\n const role = body.role === \"admin\" ? \"admin\" : \"member\";\n await store.putMember({ ...existing, role });\n return c.json({ members: await store.listMembers(org) });\n });\n\n app.delete(\"/admin/orgs/:org/members/:login\", async (c) => {\n const org = decodeURIComponent(c.req.param(\"org\"));\n if (!(await canManageMembers(c, org))) return c.json({ error: \"Forbidden.\" }, 403);\n const login = decodeURIComponent(c.req.param(\"login\")).toLowerCase();\n await store.deleteMember(org, login);\n log({ level: \"info\", event: \"member_removed\", org, login });\n return c.json({ members: await store.listMembers(org) });\n });\n\n /** The orgs the caller belongs to besides their own (for an \"shared with me\" view). */\n app.get(\"/admin/my-orgs\", async (c) => {\n const login = loginOf(c);\n const memberships = login ? await store.membershipsForLogin(login) : [];\n return c.json({ memberships });\n });\n\n app.get(\"/admin/tenants/:id/conversations\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const conversations = sessions ? await sessions.listByTenant(record.id) : [];\n return c.json({ conversations: conversations.map(publicConversationSummary) });\n });\n\n app.get(\"/admin/tenants/:id/conversations/:conversationId\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const conversation = sessions ? await sessions.get(c.req.param(\"conversationId\")) : undefined;\n if (!conversation || conversation.tenantId !== record.id) return c.json({ error: \"No such conversation.\" }, 404);\n return c.json({ conversation: publicConversation(conversation) });\n });\n\n app.get(\"/admin/tenants/:id\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n return c.json({ tenant: toPublicTenant(record) });\n });\n\n /** Return the current signing secret to an authenticated workspace member. */\n app.get(\"/admin/tenants/:id/jwt-secret\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n if (!record.jwtSecret) return c.json({ error: \"This workspace has no JWT secret.\" }, 404);\n return c.json({ secrets: { jwtSecret: record.jwtSecret } });\n });\n\n /**\n * Delete a workspace for good. Refused while the workspace is locked — the\n * lock is the guard rail against an accidental (or agent-initiated) deletion,\n * so it must be lifted deliberately first.\n */\n app.delete(\"/admin/tenants/:id\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n if (record.locked) {\n return c.json({ error: \"This workspace is locked. Unlock it before deleting.\" }, 409);\n }\n await store.delete(record.id);\n log({ level: \"info\", event: \"tenant_deleted\", tenant: record.id });\n return c.json({ ok: true, id: record.id });\n });\n\n /** Lock or unlock a workspace. A locked workspace can't be deleted. */\n app.put(\"/admin/tenants/:id/lock\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const body = (await readJson<{ locked?: boolean }>(c)) ?? {};\n const updated: TenantRecord = { ...record, locked: Boolean(body.locked), updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_lock_set\", tenant: record.id, locked: updated.locked });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /* ---------------------------- connection ---------------------------- */\n\n\n /** Persist the SDK installation for a workspace so returning users resume the same PR. */\n app.put(\"/admin/tenants/:id/repository-install\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const body = (await readJson<RepositoryInstallBody>(c)) ?? {};\n const repository = String(body.repository ?? \"\").trim();\n if (!/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(repository)) {\n return c.json({ error: \"Enter a valid GitHub repository.\" }, 400);\n }\n const status = body.status === \"submitted\" || body.status === \"failed\" ? body.status : \"installing\";\n const pullRequestUrl = typeof body.pullRequestUrl === \"string\" && /^https:\\/\\/github\\.com\\/[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+\\/pull\\/\\d+\\/?$/.test(body.pullRequestUrl)\n ? body.pullRequestUrl\n : undefined;\n const updated: TenantRecord = {\n ...record,\n repositoryInstall: {\n repository,\n status,\n ...(typeof body.runId === \"string\" && body.runId ? { runId: body.runId } : {}),\n ...(pullRequestUrl ? { pullRequestUrl } : {}),\n requiresJwtSecret: Boolean(body.requiresJwtSecret),\n ...(typeof body.failureMessage === \"string\" && body.failureMessage ? { failureMessage: body.failureMessage } : {}),\n updatedAt: new Date().toISOString(),\n },\n updatedAt: new Date().toISOString(),\n };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_repository_install_updated\", tenant: record.id, status });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Set how the tenant's product connects: MCP servers, an OpenAPI source, or neither. */\n app.put(\"/admin/tenants/:id/connection\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const body = (await readJson<ConnectionBody>(c)) ?? {};\n let config: ReturnType<typeof parseFrankieConfig>;\n try {\n config = parseFrankieConfig({\n ...record.config,\n mcpServers: body.mcpServers === undefined ? record.config.mcpServers : mergeSources(record.config.mcpServers, body.mcpServers),\n apis: body.apis === undefined ? record.config.apis : mergeSources(record.config.apis, body.apis),\n });\n } catch (error) {\n return c.json({ error: `Invalid connection: ${(error as Error).message}` }, 400);\n }\n\n // Connecting a source means \"let the agent use it\": add each source's prefix\n // to the allow list. Tool policy stays deny-by-default, but the tools you\n // just connected are no longer denied by it — otherwise they generate and\n // are immediately filtered out, and the connection does nothing. Methods\n // still default to GET-only, so this is read access, not a blank cheque.\n // Tools are named `api__<name>__<op>` / `mcp__<name>__<tool>`; naming the\n // `api__<name>` / `mcp__<name>` prefix allows every tool from that source.\n const allow = new Set(config.agent.tools.allow);\n for (const api of config.apis) allow.add(`api__${api.name}`);\n for (const server of config.mcpServers) allow.add(`mcp__${server.name}`);\n config = parseFrankieConfig({\n ...config,\n agent: { ...config.agent, tools: { ...config.agent.tools, allow: [...allow] } },\n });\n\n // Persist a read-only inventory so the dashboard can explain actual chat\n // access. If a source is temporarily unreachable, saving it still works and\n // the UI says that operation details are unavailable rather than guessing.\n const apisWithCapabilities = await Promise.all(\n config.apis.map(async (api) => {\n try {\n const capabilities = await inspectApiCapabilities(api, {\n policy: config.agent.tools,\n fetchImpl: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(8_000) }),\n });\n return { ...api, capabilities };\n } catch (error) {\n log({ level: \"warn\", event: \"api_capability_inspection_failed\", tenant: record.id, api: api.name, error: error instanceof Error ? error.message : String(error) });\n return { ...api, capabilities: [] };\n }\n }),\n );\n config = parseFrankieConfig({ ...config, apis: apisWithCapabilities });\n\n const updated: TenantRecord = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_connection_updated\", tenant: record.id });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Select which configured sources the agent may use. Platform tools stay separately gated. */\n app.put(\"/admin/tenants/:id/tool-selection\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const body = (await readJson<{ sourceIds?: unknown }>(c)) ?? {};\n const requested = Array.isArray(body.sourceIds) ? body.sourceIds.filter((id): id is string => typeof id === \"string\") : [];\n const sources = [\n ...record.config.mcpServers.map((source) => ({ id: \"mcp:\" + source.name, prefix: \"mcp__\" + source.name })),\n ...record.config.apis.map((source) => ({ id: \"openapi:\" + source.name, prefix: \"api__\" + source.name })),\n ];\n const known = new Map(sources.map((source) => [source.id, source]));\n if (requested.some((id) => !known.has(id))) return c.json({ error: \"One of the selected tool sources no longer exists.\" }, 400);\n const sourcePrefixes = new Set(sources.map((source) => source.prefix));\n const otherAllow = record.config.agent.tools.allow.filter((entry) => ![...sourcePrefixes].some((prefix) => entry === prefix || entry.startsWith(prefix + \"__\")));\n const selectedAllow = requested.map((id) => known.get(id)!.prefix);\n const config = parseFrankieConfig({\n ...record.config,\n agent: { ...record.config.agent, tools: { ...record.config.agent.tools, allow: [...otherAllow, ...selectedAllow] } },\n apis: record.config.apis.map((s) => ({ ...s, enabled: requested.includes(\"openapi:\" + s.name) })),\n mcpServers: record.config.mcpServers.map((s) => ({ ...s, enabled: requested.includes(\"mcp:\" + s.name) })),\n });\n const updated: TenantRecord = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_tool_selection_updated\", tenant: record.id, sources: requested });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Metadata-only discovery using source credentials, never a fabricated end-user token. */\n app.post(\"/admin/tenants/:id/tool-discovery\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const body = await readJson<{ name?: string }>(c);\n const source = record.config.mcpServers.find((s) => s.name === body?.name);\n if (!source) return c.json({ error: \"MCP source not found.\" }, 404);\n const session: ChatSession = { id: \"inventory\", tenantId: record.id,\n user: { id: \"inventory\", anonymous: true, claims: {} }, context: {}, messages: [],\n createdAt: Date.now(), lastActiveAt: Date.now(), messageCount: 0, costUsd: 0, busy: false };\n const result = await connectMcpTools({ servers: [{ ...source, enabled: true, selectedTools: undefined }],\n session, policy: { allow: [\"mcp__\" + source.name], deny: [], denyByDefault: true }, connectTimeoutMs: 8000 });\n try {\n if (result.failed.length) return c.json({ error: \"Tool discovery could not connect. If this MCP requires a signed-in user, enter tool names from its tools/list response; Frankie will validate availability when that user chats.\" }, 422);\n const discoveredTools = result.tools.map((tool) => ({ name: tool.name.slice((\"mcp__\" + source.name + \"__\").length), description: tool.description }));\n const config = parseFrankieConfig({ ...record.config, mcpServers: record.config.mcpServers.map((s) => s.name === source.name ? { ...s, discoveredTools } : s) });\n const updated = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n return c.json({ tenant: toPublicTenant(updated) });\n } finally { await result.close(); }\n });\n\n /** Edit one source's operation allowlist without changing other connections. */\n app.put(\"/admin/tenants/:id/tool-access\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n const body = await readJson<{ kind?: string; name?: string; selectedTools?: unknown; exposeAsMcp?: boolean }>(c);\n if (!body || ![\"openapi\", \"mcp\"].includes(body.kind ?? \"\") ||\n !Array.isArray(body.selectedTools) || !body.selectedTools.every((v) => typeof v === \"string\" && v.length > 0 && !v.includes(\"*\")))\n return c.json({ error: \"Choose specific operations or tool names.\" }, 400);\n const selectedTools = [...new Set(body.selectedTools as string[])];\n const source = body.kind === \"openapi\" ? record.config.apis.find((s) => s.name === body.name) : record.config.mcpServers.find((s) => s.name === body.name);\n if (!source) return c.json({ error: \"Source not found.\" }, 404);\n if (body.kind === \"openapi\") {\n const api = record.config.apis.find((s) => s.name === body.name)!;\n if (selectedTools.some((id) => !api.capabilities.some((operation) => operation.operationId === id)))\n return c.json({ error: \"An operation changed. Refresh the source and choose again.\" }, 400);\n }\n const config = parseFrankieConfig({\n ...record.config,\n apis: record.config.apis.map((s) => body.kind === \"openapi\" && s.name === body.name\n ? { ...s, selectedTools, ...(typeof body.exposeAsMcp === \"boolean\" ? { exposeAsMcp: body.exposeAsMcp } : {}) } : s),\n mcpServers: record.config.mcpServers.map((s) => body.kind === \"mcp\" && s.name === body.name ? { ...s, selectedTools } : s),\n });\n const updated = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Set the sites allowed to open a chat for this tenant. */\n app.put(\"/admin/tenants/:id/origins\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const body = (await readJson<{ allowedOrigins?: string[] }>(c)) ?? {};\n let config;\n try {\n config = parseFrankieConfig({\n ...record.config,\n auth: { ...record.config.auth, allowedOrigins: body.allowedOrigins ?? [] },\n });\n } catch (error) {\n return c.json({ error: `Invalid origins: ${(error as Error).message}` }, 400);\n }\n\n const updated: TenantRecord = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Set the customer's brand theme for the widget (colors, radius, light/dark). */\n app.put(\"/admin/tenants/:id/theme\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const body = (await readJson<{ theme?: unknown }>(c)) ?? {};\n let config;\n try {\n config = parseFrankieConfig({ ...record.config, theme: body.theme ?? {} });\n } catch (error) {\n return c.json({ error: `Invalid theme: ${(error as Error).message}` }, 400);\n }\n\n const updated: TenantRecord = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_theme_updated\", tenant: record.id });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /** Set the opt-in chat capabilities (attachments, model picker) for the widget. */\n app.put(\"/admin/tenants/:id/features\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const body = (await readJson<{ features?: unknown }>(c)) ?? {};\n let config;\n try {\n config = parseFrankieConfig({ ...record.config, features: body.features ?? {} });\n } catch (error) {\n return c.json({ error: `Invalid features: ${(error as Error).message}` }, 400);\n }\n\n const updated: TenantRecord = { ...record, config, updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_features_updated\", tenant: record.id });\n return c.json({ tenant: toPublicTenant(updated) });\n });\n\n /**\n * Rotate the tenant's JWT secret and return the new value. The current value\n * can also be revealed through the authenticated endpoint above. Rotating\n * invalidates any user token signed with the old secret, so the customer must\n * update `FRANKIE_JWT_SECRET` on their backend to the new value.\n */\n app.post(\"/admin/tenants/:id/rotate-jwt-secret\", async (c) => {\n const record = await own(c);\n if (!record) return c.json({ error: \"No such tenant.\" }, 404);\n\n const jwtSecret = randomBytes(36).toString(\"base64\");\n const updated: TenantRecord = { ...record, jwtSecret, updatedAt: new Date().toISOString() };\n await store.put(updated);\n log({ level: \"info\", event: \"tenant_secret_rotated\", tenant: record.id });\n\n return c.json({ tenant: toPublicTenant(updated), secrets: { jwtSecret } });\n });\n\n // Surface the real cause of an unhandled failure (usually a database\n // connection error). The admin API is token-gated, so returning the message\n // is safe and makes the dashboard show what actually went wrong.\n app.onError((error, c) => {\n const message = error instanceof Error ? error.message : \"Internal error.\";\n log({ level: \"error\", event: \"control_error\", message });\n return c.json({ error: `Control plane error: ${message}` }, 500);\n });\n\n return app;\n}\n\n/* -------------------------------- types --------------------------------- */\n\ninterface CreateTenantBody {\n name?: string;\n allowedOrigins?: string[];\n goal?: string;\n greeting?: string;\n /** Let visitors chat without a host-signed token — for public onboarding pages. */\n allowAnonymous?: boolean;\n}\n\ninterface ConnectionBody {\n mcpServers?: unknown[];\n apis?: unknown[];\n}\n\ninterface RepositoryInstallBody {\n repository?: string;\n status?: \"installing\" | \"submitted\" | \"failed\";\n runId?: string;\n pullRequestUrl?: string;\n requiresJwtSecret?: boolean;\n failureMessage?: string;\n}\n\nfunction conversationTitle(session: ChatSession): string {\n return session.title?.trim() || \"New chat\";\n}\n\nfunction publicConversationSummary(session: ChatSession) {\n return {\n id: session.id,\n title: conversationTitle(session),\n createdAt: session.createdAt,\n updatedAt: session.lastActiveAt,\n messageCount: session.messageCount,\n user: { id: session.user.id, ...(session.user.displayName ? { displayName: session.user.displayName } : {}), anonymous: session.user.anonymous },\n };\n}\n\nfunction publicConversation(session: ChatSession) {\n return {\n ...publicConversationSummary(session),\n // New UI transcripts are stored in clientMessages. Older/interrupted turns\n // still have the durable agent transcript, so expose that instead of showing\n // a conversation row with an empty detail page.\n messages: Array.isArray(session.clientMessages) && session.clientMessages.length\n ? session.clientMessages\n : session.messages,\n ...(session.busy ? { busy: true } : {}),\n };\n}\n\n/* -------------------------------- helpers -------------------------------- */\n\nasync function readJson<T>(c: Context): Promise<T | null> {\n try {\n return (await c.req.json()) as T;\n } catch {\n return null;\n }\n}\n\nexport function slug(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, 40) || \"tenant\"\n );\n}\n\n/** An empty array explicitly disconnects a kind; an update replaces only named sources. */\nfunction mergeSources<T extends { name: string }>(current: T[], updates: unknown[]): unknown[] {\n if (!updates.length) return [];\n const names = new Set(updates.map((value) => typeof value === \"object\" && value !== null ? (value as { name?: unknown }).name : undefined));\n return [...current.filter((source) => !names.has(source.name)), ...updates.map((value) => {\n if (typeof value !== \"object\" || value === null) return value;\n const previous = current.find((s) => s.name === (value as { name?: string }).name) as (T & { selectedTools?: string[]; enabled?: boolean }) | undefined;\n return { ...previous, ...value };\n })];\n}\n"],"mappings":"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","names":[]}
|