@env-hopper/backend-core 2.0.1-alpha.2 → 2.0.1-alpha.4

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
@@ -13,6 +13,8 @@ import { stepCountIs, streamText, tool } from "ai";
13
13
  import multer from "multer";
14
14
  import { readFileSync, readdirSync } from "node:fs";
15
15
  import { extname, join } from "node:path";
16
+ import express, { Router } from "express";
17
+ import * as trpcExpress from "@trpc/server/adapters/express";
16
18
 
17
19
  //#region src/db/client.ts
18
20
  let prismaClient = null;
@@ -25,6 +27,13 @@ function getDbClient() {
25
27
  return prismaClient;
26
28
  }
27
29
  /**
30
+ * Sets the internal Prisma client instance.
31
+ * Used by middleware to bridge with existing getDbClient() usage.
32
+ */
33
+ function setDbClient(client) {
34
+ prismaClient = client;
35
+ }
36
+ /**
28
37
  * Connects to the database.
29
38
  * Call this before performing database operations.
30
39
  */
@@ -1802,5 +1811,370 @@ async function syncScreenshotsFromDirectory(prisma, screenshotsDir) {
1802
1811
  }
1803
1812
 
1804
1813
  //#endregion
1805
- export { DEFAULT_ADMIN_SYSTEM_PROMPT, TABLE_SYNC_MAGAZINE, connectDb, createAdminChatHandler, createAppCatalogAdminRouter, createAuth, createAuthRouter, createDatabaseTools, createEhTrpcContext, createPrismaDatabaseClient, createScreenshotRouter, createTrpcRouter, disconnectDb, getAdminGroupsFromEnv, getAssetByName, getAuthPluginsFromEnv, getAuthProvidersFromEnv, getDbClient, getUserGroups, isAdmin, isMemberOfAllGroups, isMemberOfAnyGroup, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, requireAdmin, requireGroups, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, tool, upsertIcon, upsertIcons, validateAuthConfig };
1814
+ //#region src/middleware/database.ts
1815
+ /**
1816
+ * Formats a database connection URL from structured config.
1817
+ */
1818
+ function formatConnectionUrl(config) {
1819
+ if ("url" in config) return config.url;
1820
+ const { host, port, database, username, password, schema = "public" } = config;
1821
+ return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`;
1822
+ }
1823
+ /**
1824
+ * Internal database manager used by the middleware.
1825
+ * Handles connection URL formatting and lifecycle.
1826
+ */
1827
+ var EhDatabaseManager = class {
1828
+ constructor(config) {
1829
+ this.client = null;
1830
+ this.config = config;
1831
+ }
1832
+ /**
1833
+ * Get or create the Prisma client instance.
1834
+ * Uses lazy initialization for flexibility.
1835
+ */
1836
+ getClient() {
1837
+ if (!this.client) {
1838
+ this.client = new PrismaClient({
1839
+ datasourceUrl: formatConnectionUrl(this.config),
1840
+ log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["warn", "error"]
1841
+ });
1842
+ setDbClient(this.client);
1843
+ }
1844
+ return this.client;
1845
+ }
1846
+ async connect() {
1847
+ await this.getClient().$connect();
1848
+ }
1849
+ async disconnect() {
1850
+ if (this.client) {
1851
+ await this.client.$disconnect();
1852
+ this.client = null;
1853
+ }
1854
+ }
1855
+ };
1856
+
1857
+ //#endregion
1858
+ //#region src/middleware/backendResolver.ts
1859
+ /**
1860
+ * Type guard to check if an object implements EhBackendCompanySpecificBackend.
1861
+ */
1862
+ function isBackendInstance(obj) {
1863
+ return typeof obj === "object" && obj !== null && typeof obj.getBootstrapData === "function" && typeof obj.getAvailabilityMatrix === "function" && typeof obj.getNameMigrations === "function" && typeof obj.getResourceJumps === "function" && typeof obj.getResourceJumpsExtended === "function";
1864
+ }
1865
+ /**
1866
+ * Normalizes different backend provider types into a consistent async factory function.
1867
+ * Supports:
1868
+ * - Direct object implementing EhBackendCompanySpecificBackend
1869
+ * - Sync factory function that returns the backend
1870
+ * - Async factory function that returns the backend
1871
+ */
1872
+ function createBackendResolver(provider) {
1873
+ if (isBackendInstance(provider)) return async () => provider;
1874
+ if (typeof provider === "function") return async () => {
1875
+ const result = provider();
1876
+ return result instanceof Promise ? result : result;
1877
+ };
1878
+ throw new Error("Invalid backend provider: must be an object implementing EhBackendCompanySpecificBackend or a factory function");
1879
+ }
1880
+
1881
+ //#endregion
1882
+ //#region src/modules/appCatalogAdmin/catalogBackupController.ts
1883
+ /**
1884
+ * Export the complete app catalog as JSON
1885
+ * Includes all fields from DbAppForCatalog
1886
+ */
1887
+ async function exportCatalog(_req, res) {
1888
+ try {
1889
+ const apps = await getDbClient().dbAppForCatalog.findMany({ orderBy: { slug: "asc" } });
1890
+ res.json({
1891
+ version: "1.0",
1892
+ exportDate: (/* @__PURE__ */ new Date()).toISOString(),
1893
+ apps
1894
+ });
1895
+ } catch (error) {
1896
+ console.error("Error exporting catalog:", error);
1897
+ res.status(500).json({ error: "Failed to export catalog" });
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Import/restore the complete app catalog from JSON
1902
+ * Overwrites existing data
1903
+ */
1904
+ async function importCatalog(req, res) {
1905
+ try {
1906
+ const prisma = getDbClient();
1907
+ const { apps } = req.body;
1908
+ if (!Array.isArray(apps)) {
1909
+ res.status(400).json({ error: "Invalid data format: apps must be an array" });
1910
+ return;
1911
+ }
1912
+ await prisma.$transaction(async (tx) => {
1913
+ await tx.dbAppForCatalog.deleteMany({});
1914
+ for (const app of apps) {
1915
+ const { id, createdAt, updatedAt,...appData } = app;
1916
+ await tx.dbAppForCatalog.create({ data: appData });
1917
+ }
1918
+ });
1919
+ res.json({
1920
+ success: true,
1921
+ imported: apps.length
1922
+ });
1923
+ } catch (error) {
1924
+ console.error("Error importing catalog:", error);
1925
+ res.status(500).json({ error: "Failed to import catalog" });
1926
+ }
1927
+ }
1928
+ /**
1929
+ * Export an asset (icon or screenshot) by name
1930
+ */
1931
+ async function exportAsset(req, res) {
1932
+ try {
1933
+ const { name } = req.params;
1934
+ const asset = await getDbClient().dbAsset.findUnique({ where: { name } });
1935
+ if (!asset) {
1936
+ res.status(404).json({ error: "Asset not found" });
1937
+ return;
1938
+ }
1939
+ res.set("Content-Type", asset.mimeType);
1940
+ res.set("Content-Disposition", `attachment; filename="${name}"`);
1941
+ res.send(Buffer.from(asset.content));
1942
+ } catch (error) {
1943
+ console.error("Error exporting asset:", error);
1944
+ res.status(500).json({ error: "Failed to export asset" });
1945
+ }
1946
+ }
1947
+ /**
1948
+ * List all assets with metadata
1949
+ */
1950
+ async function listAssets(_req, res) {
1951
+ try {
1952
+ const assets = await getDbClient().dbAsset.findMany({
1953
+ select: {
1954
+ id: true,
1955
+ name: true,
1956
+ assetType: true,
1957
+ mimeType: true,
1958
+ fileSize: true,
1959
+ width: true,
1960
+ height: true,
1961
+ checksum: true
1962
+ },
1963
+ orderBy: { name: "asc" }
1964
+ });
1965
+ res.json({ assets });
1966
+ } catch (error) {
1967
+ console.error("Error listing assets:", error);
1968
+ res.status(500).json({ error: "Failed to list assets" });
1969
+ }
1970
+ }
1971
+ /**
1972
+ * Import an asset (icon or screenshot)
1973
+ */
1974
+ async function importAsset(req, res) {
1975
+ try {
1976
+ const file = req.file;
1977
+ const { name, assetType, mimeType, width, height } = req.body;
1978
+ if (!file) {
1979
+ res.status(400).json({ error: "No file uploaded" });
1980
+ return;
1981
+ }
1982
+ const prisma = getDbClient();
1983
+ const checksum = (await import("node:crypto")).createHash("sha256").update(file.buffer).digest("hex");
1984
+ const content = new Uint8Array(file.buffer);
1985
+ await prisma.dbAsset.upsert({
1986
+ where: { name },
1987
+ update: {
1988
+ content,
1989
+ checksum,
1990
+ mimeType: mimeType || file.mimetype,
1991
+ fileSize: file.size,
1992
+ width: width ? parseInt(width) : null,
1993
+ height: height ? parseInt(height) : null,
1994
+ assetType: assetType || "icon"
1995
+ },
1996
+ create: {
1997
+ name,
1998
+ content,
1999
+ checksum,
2000
+ mimeType: mimeType || file.mimetype,
2001
+ fileSize: file.size,
2002
+ width: width ? parseInt(width) : null,
2003
+ height: height ? parseInt(height) : null,
2004
+ assetType: assetType || "icon"
2005
+ }
2006
+ });
2007
+ res.json({
2008
+ success: true,
2009
+ name,
2010
+ size: file.size
2011
+ });
2012
+ } catch (error) {
2013
+ console.error("Error importing asset:", error);
2014
+ res.status(500).json({ error: "Failed to import asset" });
2015
+ }
2016
+ }
2017
+
2018
+ //#endregion
2019
+ //#region src/middleware/featureRegistry.ts
2020
+ const FEATURES = [
2021
+ {
2022
+ name: "auth",
2023
+ defaultEnabled: true,
2024
+ register: (router$1, options, ctx) => {
2025
+ const basePath = options.basePath;
2026
+ router$1.get(`${basePath}/auth/session`, async (req, res) => {
2027
+ try {
2028
+ const session = await ctx.auth.api.getSession({ headers: req.headers });
2029
+ if (session) res.json(session);
2030
+ else res.status(401).json({ error: "Not authenticated" });
2031
+ } catch (error) {
2032
+ console.error("[Auth Session Error]", error);
2033
+ res.status(500).json({ error: "Internal server error" });
2034
+ }
2035
+ });
2036
+ const authHandler = toNodeHandler(ctx.auth);
2037
+ router$1.all(`${basePath}/auth/{*any}`, authHandler);
2038
+ }
2039
+ },
2040
+ {
2041
+ name: "icons",
2042
+ defaultEnabled: true,
2043
+ register: (router$1, options) => {
2044
+ registerIconRestController(router$1, { basePath: `${options.basePath}/icons` });
2045
+ }
2046
+ },
2047
+ {
2048
+ name: "assets",
2049
+ defaultEnabled: true,
2050
+ register: (router$1, options) => {
2051
+ registerAssetRestController(router$1, { basePath: `${options.basePath}/assets` });
2052
+ }
2053
+ },
2054
+ {
2055
+ name: "screenshots",
2056
+ defaultEnabled: true,
2057
+ register: (router$1, options) => {
2058
+ registerScreenshotRestController(router$1, { basePath: `${options.basePath}/screenshots` });
2059
+ }
2060
+ },
2061
+ {
2062
+ name: "adminChat",
2063
+ defaultEnabled: false,
2064
+ register: (router$1, options) => {
2065
+ if (options.adminChat) router$1.post(`${options.basePath}/admin/chat`, createAdminChatHandler(options.adminChat));
2066
+ }
2067
+ },
2068
+ {
2069
+ name: "legacyIconEndpoint",
2070
+ defaultEnabled: false,
2071
+ register: (router$1) => {
2072
+ router$1.get("/static/icon/:icon", async (req, res) => {
2073
+ const { icon } = req.params;
2074
+ if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {
2075
+ res.status(400).send("Invalid icon name");
2076
+ return;
2077
+ }
2078
+ try {
2079
+ const dbIcon = await getAssetByName(icon);
2080
+ if (!dbIcon) {
2081
+ res.status(404).send("Icon not found");
2082
+ return;
2083
+ }
2084
+ res.setHeader("Content-Type", dbIcon.mimeType);
2085
+ res.setHeader("Cache-Control", "public, max-age=86400");
2086
+ res.send(dbIcon.content);
2087
+ } catch (error) {
2088
+ console.error("Error fetching icon:", error);
2089
+ res.status(404).send("Icon not found");
2090
+ }
2091
+ });
2092
+ }
2093
+ },
2094
+ {
2095
+ name: "catalogBackup",
2096
+ defaultEnabled: true,
2097
+ register: (router$1, options) => {
2098
+ const basePath = options.basePath;
2099
+ const upload$2 = multer({ storage: multer.memoryStorage() });
2100
+ router$1.get(`${basePath}/catalog/backup/export`, exportCatalog);
2101
+ router$1.post(`${basePath}/catalog/backup/import`, importCatalog);
2102
+ router$1.get(`${basePath}/catalog/backup/assets`, listAssets);
2103
+ router$1.get(`${basePath}/catalog/backup/assets/:name`, exportAsset);
2104
+ router$1.post(`${basePath}/catalog/backup/assets`, upload$2.single("file"), importAsset);
2105
+ }
2106
+ }
2107
+ ];
2108
+ /**
2109
+ * Registers all enabled features on the router.
2110
+ */
2111
+ function registerFeatures(router$1, options, context) {
2112
+ const toggles = options.features || {};
2113
+ for (const feature of FEATURES) {
2114
+ const isEnabled = toggles[feature.name] ?? feature.defaultEnabled;
2115
+ if (feature.name === "adminChat" && !options.adminChat) continue;
2116
+ if (isEnabled) feature.register(router$1, options, context);
2117
+ }
2118
+ }
2119
+
2120
+ //#endregion
2121
+ //#region src/middleware/createEhMiddleware.ts
2122
+ async function createEhMiddleware(options) {
2123
+ var _normalizedOptions$fe, _options$hooks;
2124
+ const basePath = options.basePath ?? "/api";
2125
+ const normalizedOptions = {
2126
+ ...options,
2127
+ basePath
2128
+ };
2129
+ const dbManager = new EhDatabaseManager(options.database);
2130
+ dbManager.getClient();
2131
+ const auth = createAuth({
2132
+ appName: options.auth.appName,
2133
+ baseURL: options.auth.baseURL,
2134
+ secret: options.auth.secret,
2135
+ providers: options.auth.providers,
2136
+ plugins: options.auth.plugins,
2137
+ sessionExpiresIn: options.auth.sessionExpiresIn,
2138
+ sessionUpdateAge: options.auth.sessionUpdateAge
2139
+ });
2140
+ const trpcRouter = createTrpcRouter(auth);
2141
+ const resolveBackend = createBackendResolver(options.backend);
2142
+ const createContext = async () => {
2143
+ return createEhTrpcContext({ companySpecificBackend: await resolveBackend() });
2144
+ };
2145
+ const router$1 = Router();
2146
+ router$1.use(express.json());
2147
+ const middlewareContext = {
2148
+ auth,
2149
+ trpcRouter,
2150
+ createContext
2151
+ };
2152
+ if (((_normalizedOptions$fe = normalizedOptions.features) === null || _normalizedOptions$fe === void 0 ? void 0 : _normalizedOptions$fe.trpc) !== false) router$1.use(`${basePath}/trpc`, trpcExpress.createExpressMiddleware({
2153
+ router: trpcRouter,
2154
+ createContext
2155
+ }));
2156
+ registerFeatures(router$1, normalizedOptions, middlewareContext);
2157
+ if ((_options$hooks = options.hooks) === null || _options$hooks === void 0 ? void 0 : _options$hooks.onRoutesRegistered) await options.hooks.onRoutesRegistered(router$1);
2158
+ return {
2159
+ router: router$1,
2160
+ auth,
2161
+ trpcRouter,
2162
+ async connect() {
2163
+ var _options$hooks2;
2164
+ await dbManager.connect();
2165
+ if ((_options$hooks2 = options.hooks) === null || _options$hooks2 === void 0 ? void 0 : _options$hooks2.onDatabaseConnected) await options.hooks.onDatabaseConnected();
2166
+ },
2167
+ async disconnect() {
2168
+ var _options$hooks3;
2169
+ if ((_options$hooks3 = options.hooks) === null || _options$hooks3 === void 0 ? void 0 : _options$hooks3.onDatabaseDisconnecting) await options.hooks.onDatabaseDisconnecting();
2170
+ await dbManager.disconnect();
2171
+ },
2172
+ addRoutes(callback) {
2173
+ callback(router$1);
2174
+ }
2175
+ };
2176
+ }
2177
+
2178
+ //#endregion
2179
+ export { DEFAULT_ADMIN_SYSTEM_PROMPT, EhDatabaseManager, TABLE_SYNC_MAGAZINE, connectDb, createAdminChatHandler, createAppCatalogAdminRouter, createAuth, createAuthRouter, createDatabaseTools, createEhMiddleware, createEhTrpcContext, createPrismaDatabaseClient, createScreenshotRouter, createTrpcRouter, disconnectDb, getAdminGroupsFromEnv, getAssetByName, getAuthPluginsFromEnv, getAuthProvidersFromEnv, getDbClient, getUserGroups, isAdmin, isMemberOfAllGroups, isMemberOfAnyGroup, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, requireAdmin, requireGroups, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, tool, upsertIcon, upsertIcons, validateAuthConfig };
1806
2180
  //# sourceMappingURL=index.js.map