@exulu/backend 3.3.0 → 3.4.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/index.js CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  ExuluTool,
15
15
  KB_EDITOR_TOOL_ID,
16
16
  LITELLM_UI_PATH,
17
- LiteLLMAdminError,
18
17
  OAUTH_CALLBACK_PATH,
19
18
  PreviewRenderError,
20
19
  ResolveModelError,
@@ -88,10 +87,12 @@ import {
88
87
  verifyCredentialNonce,
89
88
  waitForLiteLLMReady,
90
89
  withRetry
91
- } from "./chunk-5FTX543Z.js";
90
+ } from "./chunk-UIIUBZCO.js";
92
91
  import {
93
- findLiteLLMModel
94
- } from "./chunk-7CCMW3IW.js";
92
+ LiteLLMAdminError,
93
+ findLiteLLMModel,
94
+ resolveLiteLLMTarget
95
+ } from "./chunk-NUVMV5FC.js";
95
96
 
96
97
  // src/index.ts
97
98
  import "dotenv/config";
@@ -1216,15 +1217,7 @@ async function resolveEmbedder(input) {
1216
1217
  `LiteLLM is not ready: ${err.message}`
1217
1218
  );
1218
1219
  }
1219
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
1220
- const port = process.env.LITELLM_PORT ?? "4000";
1221
- const masterKey = process.env.LITELLM_MASTER_KEY;
1222
- if (!masterKey) {
1223
- throw new ResolveEmbedderError(
1224
- "LITELLM_NOT_CONFIGURED",
1225
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
1226
- );
1227
- }
1220
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
1228
1221
  const resolvedUserId = user?.id ?? userId;
1229
1222
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
1230
1223
  const { dimensionality, maxChunkSize, maxBatchSize } = getEmbeddingModelInfo(model);
@@ -1245,12 +1238,12 @@ async function resolveEmbedder(input) {
1245
1238
  routine_name: routine?.name,
1246
1239
  context_name: contextName
1247
1240
  });
1248
- const endpoint = `http://${host}:${port}/v1/embeddings`;
1241
+ const endpoint = `${baseUrl}/v1/embeddings`;
1249
1242
  const embedBatch = async (batch) => {
1250
1243
  const res = await fetch(endpoint, {
1251
1244
  method: "POST",
1252
1245
  headers: {
1253
- Authorization: `Bearer ${masterKey}`,
1246
+ ...authHeaders,
1254
1247
  "Content-Type": "application/json"
1255
1248
  },
1256
1249
  body: JSON.stringify({
@@ -6199,20 +6192,25 @@ async function encryptString(string) {
6199
6192
  const hash = await bcrypt.hash(string, SALT_ROUNDS);
6200
6193
  return hash;
6201
6194
  }
6202
- var generateApiKey = async (name, email) => {
6195
+ var generateApiKey = async (name, email, options) => {
6203
6196
  const { db } = await postgresClient();
6204
6197
  email = String(email).trim().toLowerCase();
6198
+ const superAdmin = options?.superAdmin ?? true;
6199
+ const roleName = options?.roleName ?? "admin";
6200
+ const rolePermissions = options?.rolePermissions ?? {
6201
+ agents: "write",
6202
+ workflows: "write",
6203
+ variables: "write",
6204
+ users: "write"
6205
+ };
6205
6206
  console.log("[EXULU] Inserting default user and admin role.");
6206
- const existingRole = await db.from("roles").where({ name: "admin" }).first();
6207
+ const existingRole = await db.from("roles").where({ name: roleName }).first();
6207
6208
  let roleId;
6208
6209
  if (!existingRole) {
6209
- console.log("[EXULU] Creating default admin role.");
6210
+ console.log(`[EXULU] Creating default ${roleName} role.`);
6210
6211
  const role = await db.from("roles").insert({
6211
- name: "admin",
6212
- agents: "write",
6213
- workflows: "write",
6214
- variables: "write",
6215
- users: "write"
6212
+ name: roleName,
6213
+ ...rolePermissions
6216
6214
  }).returning("id");
6217
6215
  roleId = role[0].id;
6218
6216
  } else {
@@ -6228,7 +6226,7 @@ var generateApiKey = async (name, email) => {
6228
6226
  await db.from("users").insert({
6229
6227
  name,
6230
6228
  email,
6231
- super_admin: true,
6229
+ super_admin: superAdmin,
6232
6230
  createdAt: /* @__PURE__ */ new Date(),
6233
6231
  updatedAt: /* @__PURE__ */ new Date(),
6234
6232
  type: "api",
@@ -6800,6 +6798,9 @@ function createMutations(table, contexts, tools, config) {
6800
6798
  }
6801
6799
  throw new Error("Only the creator can edit this private record");
6802
6800
  }
6801
+ if (record.created_by != null && String(record.created_by) === String(user.id)) {
6802
+ return true;
6803
+ }
6803
6804
  if (record.rights_mode === "users") {
6804
6805
  const rbacRecord = await db.from("rbac").where({
6805
6806
  entity: table.name.singular,
@@ -12866,7 +12867,7 @@ type LiteLLMModel {
12866
12867
  }
12867
12868
  `;
12868
12869
  resolvers.Query["litellmCatalog"] = async () => {
12869
- const { fetchLiteLLMCatalog } = await import("./catalog-UGTDNMDM.js");
12870
+ const { fetchLiteLLMCatalog } = await import("./catalog-PZTI2MJZ.js");
12870
12871
  return fetchLiteLLMCatalog();
12871
12872
  };
12872
12873
  resolvers.Query["workflowSchedule"] = async (_, args, context, info) => {
@@ -14828,11 +14829,8 @@ var TranscriptionError = class extends Error {
14828
14829
  }
14829
14830
  };
14830
14831
  async function transcribeAudio(args) {
14831
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14832
- const port = process.env.LITELLM_PORT ?? "4000";
14833
- const masterKey = process.env.LITELLM_MASTER_KEY;
14832
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14834
14833
  const model = process.env.TRANSCRIPTION_MODEL;
14835
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14836
14834
  if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
14837
14835
  const form = new FormData();
14838
14836
  form.append(
@@ -14842,9 +14840,9 @@ async function transcribeAudio(args) {
14842
14840
  );
14843
14841
  form.append("model", model);
14844
14842
  if (args.language) form.append("language", args.language);
14845
- const res = await fetch(`http://${host}:${port}/v1/audio/transcriptions`, {
14843
+ const res = await fetch(`${baseUrl}/v1/audio/transcriptions`, {
14846
14844
  method: "POST",
14847
- headers: { Authorization: `Bearer ${masterKey}` },
14845
+ headers: { ...authHeaders },
14848
14846
  body: form
14849
14847
  });
14850
14848
  if (!res.ok) {
@@ -14867,12 +14865,9 @@ var SpeechError = class extends Error {
14867
14865
  }
14868
14866
  };
14869
14867
  async function synthesizeSpeech(args) {
14870
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14871
- const port = process.env.LITELLM_PORT ?? "4000";
14872
- const masterKey = process.env.LITELLM_MASTER_KEY;
14868
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14873
14869
  const model = process.env.TTS_MODEL;
14874
14870
  const voice = process.env.TTS_VOICE;
14875
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14876
14871
  if (!model) throw new Error("TTS_MODEL is not set");
14877
14872
  if (!voice) throw new Error("TTS_VOICE is not set");
14878
14873
  const body = {
@@ -14881,10 +14876,10 @@ async function synthesizeSpeech(args) {
14881
14876
  voice,
14882
14877
  response_format: "mp3"
14883
14878
  };
14884
- const res = await fetch(`http://${host}:${port}/v1/audio/speech`, {
14879
+ const res = await fetch(`${baseUrl}/v1/audio/speech`, {
14885
14880
  method: "POST",
14886
14881
  headers: {
14887
- Authorization: `Bearer ${masterKey}`,
14882
+ ...authHeaders,
14888
14883
  "Content-Type": "application/json"
14889
14884
  },
14890
14885
  body: JSON.stringify(body)
@@ -14908,13 +14903,6 @@ var ImageGenerationError = class extends Error {
14908
14903
  this.name = "ImageGenerationError";
14909
14904
  }
14910
14905
  };
14911
- var resolveProxyConfig = () => {
14912
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
14913
- const port = process.env.LITELLM_PORT ?? "4000";
14914
- const masterKey = process.env.LITELLM_MASTER_KEY;
14915
- if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
14916
- return { host, port, masterKey };
14917
- };
14918
14906
  var normalizeDataEntries = async (data) => {
14919
14907
  const out = [];
14920
14908
  for (const entry of data) {
@@ -14951,7 +14939,7 @@ var normalizeDataEntries = async (data) => {
14951
14939
  async function generateImage(args) {
14952
14940
  if (!args.model) throw new Error("model is required");
14953
14941
  if (!args.prompt) throw new Error("prompt is required");
14954
- const cfg = resolveProxyConfig();
14942
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14955
14943
  const body = {
14956
14944
  model: args.model,
14957
14945
  prompt: args.prompt
@@ -14959,10 +14947,10 @@ async function generateImage(args) {
14959
14947
  if (args.size) body.size = args.size;
14960
14948
  if (args.quality) body.quality = args.quality;
14961
14949
  if (args.n) body.n = args.n;
14962
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/generations`, {
14950
+ const res = await fetch(`${baseUrl}/v1/images/generations`, {
14963
14951
  method: "POST",
14964
14952
  headers: {
14965
- Authorization: `Bearer ${cfg.masterKey}`,
14953
+ ...authHeaders,
14966
14954
  "Content-Type": "application/json"
14967
14955
  },
14968
14956
  body: JSON.stringify(body),
@@ -14990,7 +14978,7 @@ async function editImage(args) {
14990
14978
  if (!args.references || args.references.length === 0) {
14991
14979
  throw new Error("at least one reference image is required");
14992
14980
  }
14993
- const cfg = resolveProxyConfig();
14981
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
14994
14982
  const form = new FormData();
14995
14983
  form.append("model", args.model);
14996
14984
  form.append("prompt", args.prompt);
@@ -15012,9 +15000,9 @@ async function editImage(args) {
15012
15000
  args.mask.filename
15013
15001
  );
15014
15002
  }
15015
- const res = await fetch(`http://${cfg.host}:${cfg.port}/v1/images/edits`, {
15003
+ const res = await fetch(`${baseUrl}/v1/images/edits`, {
15016
15004
  method: "POST",
15017
- headers: { Authorization: `Bearer ${cfg.masterKey}` },
15005
+ headers: { ...authHeaders },
15018
15006
  body: form,
15019
15007
  signal: args.signal
15020
15008
  });
@@ -15161,6 +15149,9 @@ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the
15161
15149
  // src/exulu/routes.ts
15162
15150
  import { resolve as resolvePath } from "path";
15163
15151
 
15152
+ // src/exulu/litellm/passthrough-allowlist.ts
15153
+ var isLiteLLMPassthroughPathAllowed = (path2) => path2.startsWith("/v1/") || path2 === "/model/info";
15154
+
15164
15155
  // src/exulu/litellm/usage-view.ts
15165
15156
  var DAY_MS = 24 * 60 * 60 * 1e3;
15166
15157
  var DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -17377,7 +17368,7 @@ ${style.markdown}` : params.prompt;
17377
17368
  return;
17378
17369
  }
17379
17370
  const user = authenticationResult.user;
17380
- if (!req.path.startsWith("/v1/")) {
17371
+ if (!isLiteLLMPassthroughPathAllowed(req.path)) {
17381
17372
  res.status(403).json({
17382
17373
  detail: `Path ${req.path} is not exposed through the Exulu LiteLLM proxy.`
17383
17374
  });
@@ -23395,15 +23386,7 @@ async function resolveOcr(input) {
23395
23386
  `LiteLLM is not ready: ${err.message}`
23396
23387
  );
23397
23388
  }
23398
- const host = process.env.LITELLM_HOST ?? "127.0.0.1";
23399
- const port = process.env.LITELLM_PORT ?? "4000";
23400
- const masterKey = process.env.LITELLM_MASTER_KEY;
23401
- if (!masterKey) {
23402
- throw new ResolveOcrError(
23403
- "LITELLM_NOT_CONFIGURED",
23404
- "LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
23405
- );
23406
- }
23389
+ const { baseUrl, authHeaders } = resolveLiteLLMTarget();
23407
23390
  const resolvedUserId = user?.id ?? userId;
23408
23391
  if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
23409
23392
  const role = user?.role;
@@ -23423,12 +23406,12 @@ async function resolveOcr(input) {
23423
23406
  routine_name: routine?.name,
23424
23407
  context_name: contextName
23425
23408
  });
23426
- const endpoint = `http://${host}:${port}/v1/ocr`;
23409
+ const endpoint = `${baseUrl}/v1/ocr`;
23427
23410
  const ocr = async (document, opts) => {
23428
23411
  const res = await fetch(endpoint, {
23429
23412
  method: "POST",
23430
23413
  headers: {
23431
- Authorization: `Bearer ${masterKey}`,
23414
+ ...authHeaders,
23432
23415
  "Content-Type": "application/json"
23433
23416
  },
23434
23417
  body: JSON.stringify({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.3.0",
4
+ "version": "3.4.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {