@env-hopper/backend-core 2.0.1-alpha.1 → 2.0.1-alpha.3
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.d.ts +232 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +289 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/db/client.ts +8 -0
- package/src/db/index.ts +8 -4
- package/src/index.ts +67 -52
- package/src/middleware/backendResolver.ts +49 -0
- package/src/middleware/createEhMiddleware.ts +177 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +149 -0
- package/src/middleware/index.ts +18 -0
- package/src/middleware/types.ts +167 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["prismaClient: PrismaClient | null","categories: Array<AppCategory>","duplicateKeys: Array<string>","results: Array<MakeTFromPrismaModel<TPrismaModelName>>","router","t","publicProcedure","router","t","publicProcedure","router","t","publicProcedure","providers: Array<string>","router","t","publicProcedure","data: Record<string, unknown>","t: TRPCRootObject<EhTrpcContext, {}, {}>","router: typeof t.router","publicProcedure: typeof t.procedure","z","staticControllerContract: EhStaticControllerContract","providers: BetterAuthOptions['socialProviders']","plugins: Array<BetterAuthPlugin>","oktaConfig: Array<ReturnType<typeof okta>>","t","upload","outBuffer: Uint8Array","buf: Buffer","content: Uint8Array | Buffer","width: number | null","height: number | null"],"sources":["../src/db/client.ts","../src/modules/appCatalog/service.ts","../src/db/tableSyncPrismaAdapter.ts","../src/db/tableSyncMagazine.ts","../src/db/syncAppCatalog.ts","../src/modules/appCatalogAdmin/appCatalogAdminRouter.ts","../src/modules/assets/screenshotRouter.ts","../src/modules/auth/authRouter.ts","../src/modules/assets/assetUtils.ts","../src/modules/icons/iconRouter.ts","../src/server/controller.ts","../src/server/ehTrpcContext.ts","../src/server/ehStaticControllerContract.ts","../src/modules/auth/auth.ts","../src/modules/auth/registerAuthRoutes.ts","../src/modules/auth/authProviders.ts","../src/modules/auth/authorizationUtils.ts","../src/modules/admin/chat/createAdminChatHandler.ts","../src/modules/admin/chat/createDatabaseTools.ts","../src/modules/icons/iconRestController.ts","../src/modules/icons/iconService.ts","../src/modules/assets/assetRestController.ts","../src/modules/assets/screenshotRestController.ts","../src/modules/assets/syncAssets.ts"],"sourcesContent":["import { PrismaClient } from '@prisma/client'\n\nlet prismaClient: PrismaClient | null = null\n\n/**\n * Gets the internal Prisma client instance.\n * Creates one if it doesn't exist.\n */\nexport function getDbClient(): PrismaClient {\n if (!prismaClient) {\n prismaClient = new PrismaClient()\n }\n return prismaClient\n}\n\n/**\n * Connects to the database.\n * Call this before performing database operations.\n */\nexport async function connectDb(): Promise<void> {\n const client = getDbClient()\n await client.$connect()\n}\n\n/**\n * Disconnects from the database.\n * Call this when done with database operations (e.g., in scripts).\n */\nexport async function disconnectDb(): Promise<void> {\n if (prismaClient) {\n await prismaClient.$disconnect()\n prismaClient = null\n }\n}\n","import { getDbClient } from '../../db/client'\nimport type {\n AppCatalogData,\n AppCategory,\n AppForCatalog,\n} from '../../types/common/appCatalogTypes'\n\nfunction capitalize(word: string): string {\n if (!word) return word\n return word.charAt(0).toUpperCase() + word.slice(1)\n}\n\nexport async function getAppsFromPrisma(): Promise<Array<AppForCatalog>> {\n const prisma = getDbClient()\n\n // Fetch all apps\n const rows = await prisma.dbAppForCatalog.findMany()\n\n return rows.map((row) => {\n const access = row.access as unknown as AppForCatalog['access']\n const roles =\n row.roles == null\n ? undefined\n : (row.roles as unknown as AppForCatalog['roles'])\n const teams = (row.teams as unknown as Array<string> | null) ?? []\n const tags = (row.tags as unknown as AppForCatalog['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as AppForCatalog['screenshotIds']) ?? []\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n\n return {\n id: row.id,\n slug: row.slug,\n displayName: row.displayName,\n description: row.description,\n access,\n teams,\n roles,\n approver:\n row.approverName && row.approverEmail\n ? { name: row.approverName, email: row.approverEmail }\n : undefined,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n }\n })\n}\n\nexport function deriveCategories(\n apps: Array<AppForCatalog>,\n): Array<AppCategory> {\n const tagSet = new Set<string>()\n for (const app of apps) {\n for (const tag of app.tags ?? []) {\n const normalized = tag.trim().toLowerCase()\n if (normalized) tagSet.add(normalized)\n }\n }\n const categories: Array<AppCategory> = [{ id: 'all', name: 'All' }]\n for (const tag of Array.from(tagSet).sort()) {\n categories.push({ id: tag, name: capitalize(tag) })\n }\n return categories\n}\n\nexport async function getAppCatalogData(\n getAppsOptional?: () => Promise<Array<AppForCatalog>>,\n): Promise<AppCatalogData> {\n const apps = getAppsOptional\n ? await getAppsOptional()\n : await getAppsFromPrisma()\n const categories = deriveCategories(apps)\n return { apps, categories }\n}\n","import { tableSync } from '@env-hopper/table-sync'\nimport type { Prisma, PrismaClient } from '@prisma/client'\nimport type * as runtime from '@prisma/client/runtime/library'\nimport { mapValues, omit, pick } from 'radashi'\n\nexport type ScalarKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\nexport type ObjectKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['objects']\n\nexport type ScalarFilter<TPrismaModelName extends Prisma.ModelName> = Partial<\n Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\n>\n\nexport type GetOperationFns<TModel extends Prisma.ModelName> = {\n [TOperation in keyof Prisma.TypeMap['model']['DbAppForCatalog']['operations']]: (\n args: Prisma.TypeMap['model'][TModel]['operations'][TOperation]['args'],\n ) => Promise<\n Prisma.TypeMap['model'][TModel]['operations'][TOperation]['result']\n >\n}\n\nexport interface TableSyncParamsPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,\n TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,\n> {\n prisma: TPrismaClient\n prismaModelName: TPrismaModelName\n uniqColumns: TUniqColumns\n relationColumns?: TRelationColumns\n where?: ScalarFilter<TPrismaModelName>\n upsertOnly?: boolean\n}\n\nfunction getPrismaModelOperations<\n TPrismaClient extends Omit<PrismaClient, runtime.ITXClientDenyList>,\n TPrismaModelName extends Prisma.ModelName,\n>(prisma: TPrismaClient, prismaModelName: TPrismaModelName) {\n const key = (prismaModelName.slice(0, 1).toLowerCase() +\n prismaModelName.slice(1)) as keyof TPrismaClient\n return prisma[key] as GetOperationFns<TPrismaModelName>\n}\n\nexport type MakeTFromPrismaModel<TPrismaModelName extends Prisma.ModelName> =\n NonNullable<\n Prisma.TypeMap['model'][TPrismaModelName]['operations']['findUnique']['result']\n >\n\nexport function tableSyncPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,\n TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,\n TId extends ScalarKeys<TPrismaModelName> & (string | number) = 'id',\n>(\n params: TableSyncParamsPrisma<\n TPrismaClient,\n TPrismaModelName,\n TUniqColumns,\n TRelationColumns\n >,\n) {\n const {\n prisma,\n prismaModelName,\n uniqColumns,\n where: whereGlobal,\n upsertOnly,\n } = params\n const prismOperations = getPrismaModelOperations(prisma, prismaModelName)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n return tableSync<MakeTFromPrismaModel<TPrismaModelName>, TUniqColumns, TId>({\n id: 'id' as TId,\n uniqColumns,\n readAll: async () => {\n const findManyArgs = whereGlobal\n ? {\n where: whereGlobal,\n }\n : {}\n return (await prismOperations.findMany(findManyArgs)) as Array<\n MakeTFromPrismaModel<TPrismaModelName>\n >\n },\n writeAll: async (createData, update, deleteIds) => {\n const prismaUniqKey = params.uniqColumns.join('_')\n const relationColumnList =\n params.relationColumns ?? ([] as Array<ObjectKeys<TPrismaModelName>>)\n\n return prisma.$transaction(async (tx) => {\n const txOps = getPrismaModelOperations(tx, prismaModelName)\n for (const { data, where } of update) {\n const uniqKeyWhere =\n Object.keys(where).length > 1\n ? {\n [prismaUniqKey]: where,\n }\n : where\n\n const dataScalar = omit(data, relationColumnList)\n const dataRelations = mapValues(\n pick(data, relationColumnList),\n (value) => {\n return {\n set: value,\n }\n },\n )\n\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.update({\n data: { ...dataScalar, ...dataRelations },\n where: { ...uniqKeyWhere },\n })\n }\n\n if (upsertOnly !== true) {\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.deleteMany({\n where: {\n id: {\n in: deleteIds,\n },\n },\n })\n }\n\n // Validate uniqueness of uniqColumns before creating records\n const createDataMapped = createData.map((data) => {\n // @ts-expect-error This is too difficult for me to come up with right types\n const dataScalar = omit(data, relationColumnList)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n const onlyRelationColumns = pick(data, relationColumnList)\n const dataRelations = mapValues(onlyRelationColumns, (value) => {\n return {\n connect: value,\n }\n })\n\n return { ...dataScalar, ...dataRelations }\n })\n\n // Check for duplicates in the data to be created\n if (createDataMapped.length > 0) {\n const uniqKeysInCreate = new Set<string>()\n const duplicateKeys: Array<string> = []\n\n for (const data of createDataMapped) {\n const keyParts = params.uniqColumns.map((col) => {\n const value = data[col as keyof typeof data]\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive: unique columns may be nullable in schemas\n return value === null || value === undefined\n ? 'null'\n : String(value)\n })\n const key = keyParts.join(':')\n\n if (uniqKeysInCreate.has(key)) {\n duplicateKeys.push(key)\n } else {\n uniqKeysInCreate.add(key)\n }\n }\n\n if (duplicateKeys.length > 0) {\n const uniqColumnsStr = params.uniqColumns.join(', ')\n throw new Error(\n `Duplicate unique key values found in data to be created. ` +\n `Model: ${prismaModelName}, Unique columns: [${uniqColumnsStr}], ` +\n `Duplicate keys: [${duplicateKeys.join(', ')}]`,\n )\n }\n }\n\n const results: Array<MakeTFromPrismaModel<TPrismaModelName>> = []\n\n if (relationColumnList.length === 0) {\n // @ts-expect-error This is too difficult for me to come up with right types\n const batchResult = await txOps.createManyAndReturn({\n data: createDataMapped,\n })\n\n results.push(...batchResult)\n } else {\n for (const dataMappedElement of createDataMapped) {\n // @ts-expect-error too difficult for me\n const newVar = await txOps.create({\n data: dataMappedElement,\n })\n results.push(newVar as MakeTFromPrismaModel<TPrismaModelName>)\n }\n }\n\n return results\n })\n },\n })\n}\n","import type { Prisma } from '@prisma/client'\nimport type { ObjectKeys, ScalarKeys } from './tableSyncPrismaAdapter'\n\ninterface CommonSyncTableInfo<TPrismaModelName extends Prisma.ModelName> {\n prismaModelName: TPrismaModelName\n uniqColumns: Array<ScalarKeys<TPrismaModelName>>\n relationColumns?: Array<ObjectKeys<TPrismaModelName>>\n}\n\ntype TableSyncMagazineType = Partial<{\n [key in Prisma.ModelName]: CommonSyncTableInfo<key>\n}>\n\nexport const TABLE_SYNC_MAGAZINE = {\n DbAppForCatalog: {\n prismaModelName: 'DbAppForCatalog',\n uniqColumns: ['slug'],\n },\n} as const satisfies TableSyncMagazineType\n\nexport type TableSyncMagazine = typeof TABLE_SYNC_MAGAZINE\nexport type TableSyncMagazineModelNameKey = keyof TableSyncMagazine\n","import type { AppForCatalog } from '../types/common/appCatalogTypes'\nimport { getDbClient } from './client'\nimport { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'\nimport { tableSyncPrisma } from './tableSyncPrismaAdapter'\n\nexport interface SyncAppCatalogResult {\n created: number\n updated: number\n deleted: number\n total: number\n}\n\n/**\n * Syncs app catalog data to the database using table sync.\n * This will create new apps, update existing ones, and delete any that are no longer in the input.\n *\n * Note: Call connectDb() before and disconnectDb() after if running in a script.\n */\nexport async function syncAppCatalog(\n apps: Array<AppForCatalog>,\n): Promise<SyncAppCatalogResult> {\n const prisma = getDbClient()\n\n const sync = tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbAppForCatalog,\n })\n\n // Transform AppForCatalog to DbAppForCatalog format\n const dbApps = apps.map((app) => {\n const slug =\n app.slug ||\n app.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return {\n slug,\n displayName: app.displayName,\n description: app.description,\n access: app.access,\n teams: app.teams ?? [],\n roles: app.roles ?? null,\n approverName: app.approver?.name ?? null,\n approverEmail: app.approver?.email ?? null,\n notes: app.notes ?? null,\n tags: app.tags ?? [],\n appUrl: app.appUrl ?? null,\n links: app.links ?? null,\n iconName: app.iconName ?? null,\n screenshotIds: app.screenshotIds ?? [],\n }\n })\n\n const result = await sync.sync(dbApps)\n\n // Get actual synced data to calculate stats\n const actual = result.getActual()\n\n return {\n created: actual.length - apps.length + (apps.length - actual.length),\n updated: 0, // TableSync doesn't expose this directly\n deleted: 0, // TableSync doesn't expose this directly\n total: actual.length,\n }\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\n\n// Zod schema for access method (simplified for now - you can expand this)\nconst AccessMethodSchema = z.object({\n type: z.enum(['bot', 'ticketing', 'email', 'self-service', 'documentation', 'manual']),\n}).passthrough()\n\nconst AppLinkSchema = z.object({\n displayName: z.string().optional(),\n url: z.string().url(),\n})\n\nconst AppRoleSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().optional(),\n})\n\nconst ApproverSchema = z.object({\n name: z.string(),\n email: z.string().email(),\n})\n\nconst CreateAppForCatalogSchema = z.object({\n slug: z.string().min(1).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),\n displayName: z.string().min(1),\n description: z.string(),\n access: AccessMethodSchema,\n teams: z.array(z.string()).optional(),\n roles: z.array(AppRoleSchema).optional(),\n approver: ApproverSchema.optional(),\n notes: z.string().optional(),\n tags: z.array(z.string()).optional(),\n appUrl: z.string().url().optional(),\n links: z.array(AppLinkSchema).optional(),\n iconName: z.string().optional(),\n screenshotIds: z.array(z.string()).optional(),\n})\n\nconst UpdateAppForCatalogSchema = CreateAppForCatalogSchema.partial().extend({\n id: z.string(),\n})\n\nexport function createAppCatalogAdminRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.findMany({\n orderBy: { displayName: 'asc' },\n })\n }),\n\n getById: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.findUnique({\n where: { id: input.id },\n })\n }),\n\n create: publicProcedure\n .input(CreateAppForCatalogSchema)\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { approver, ...rest } = input\n\n return prisma.dbAppForCatalog.create({\n data: {\n ...rest,\n approverName: approver?.name ?? null,\n approverEmail: approver?.email ?? null,\n teams: input.teams ?? [],\n tags: input.tags ?? [],\n screenshotIds: input.screenshotIds ?? [],\n },\n })\n }),\n\n update: publicProcedure\n .input(UpdateAppForCatalogSchema)\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { id, approver, ...rest } = input\n\n return prisma.dbAppForCatalog.update({\n where: { id },\n data: {\n ...rest,\n ...(approver !== undefined && {\n approverName: approver.name || null,\n approverEmail: approver.email || null,\n }),\n },\n })\n }),\n\n delete: publicProcedure\n .input(z.object({ id: z.string() }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.delete({\n where: { id: input.id },\n })\n }),\n })\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\n\nexport function createScreenshotRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAsset.findMany({\n where: { assetType: 'screenshot' },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n orderBy: { createdAt: 'desc' },\n })\n }),\n\n getOne: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n\n getByAppSlug: publicProcedure\n .input(z.object({ appSlug: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: input.appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app) {\n return []\n }\n\n // Fetch all screenshots for the app\n return prisma.dbAsset.findMany({\n where: {\n id: { in: app.screenshotIds },\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n\n getFirstByAppSlug: publicProcedure\n .input(z.object({ appSlug: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: input.appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app || app.screenshotIds.length === 0) {\n return null\n }\n\n // Fetch first screenshot\n return prisma.dbAsset.findUnique({\n where: { id: app.screenshotIds[0] },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n })\n}\n","import type { BetterAuthPlugin } from 'better-auth'\nimport type { TRPCRootObject } from '@trpc/server'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\nimport type { BetterAuth } from './auth'\n\n/**\n * Create auth tRPC procedures\n * @param t - tRPC instance\n * @param auth - Better Auth instance (optional, for future extensions)\n * @returns tRPC router with auth procedures\n */\nexport function createAuthRouter(\n t: TRPCRootObject<EhTrpcContext, {}, {}>,\n auth?: BetterAuth,\n) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n getSession: publicProcedure.query(async ({ ctx }) => {\n // Session will be extracted from cookies by better-auth middleware\n // For now, return user info if available in context\n const contextWithUser = ctx as EhTrpcContext & { user?: unknown }\n return {\n user: contextWithUser.user ?? null,\n isAuthenticated: !!contextWithUser.user,\n }\n }),\n getProviders: publicProcedure.query(() => {\n // Return configured social providers and OAuth providers from plugins\n const providers: Array<string> = []\n const authOptions = auth?.options\n\n // Add built-in social providers (github, google, etc.)\n if (authOptions?.socialProviders) {\n const socialProviders = authOptions.socialProviders as Record<\n string,\n unknown\n >\n Object.keys(socialProviders).forEach((key) => {\n if (socialProviders[key]) {\n providers.push(key)\n }\n })\n }\n\n // Add OAuth providers from plugins (like Okta via genericOAuth)\n if (authOptions?.plugins) {\n const plugins = authOptions.plugins\n plugins.forEach((plugin) => {\n const pluginWithConfig = plugin as BetterAuthPlugin & {\n options?: {\n config?: Array<{ providerId?: string }>\n }\n }\n if (\n pluginWithConfig.id === 'generic-oauth' &&\n pluginWithConfig.options?.config\n ) {\n const configs = Array.isArray(pluginWithConfig.options.config)\n ? pluginWithConfig.options.config\n : [pluginWithConfig.options.config]\n configs.forEach((config) => {\n if (config.providerId) {\n providers.push(config.providerId)\n }\n })\n }\n })\n }\n\n return { providers }\n }),\n })\n}\n\nexport type AuthRouter = ReturnType<typeof createAuthRouter>\n","import { createHash } from 'node:crypto';\nimport sharp from 'sharp';\n\n/**\n * Extract image dimensions from a buffer using sharp\n */\nexport async function getImageDimensions(\n buffer: Buffer,\n): Promise<{ width: number | null; height: number | null }> {\n try {\n const metadata = await sharp(buffer).metadata()\n return {\n width: metadata.width ?? null,\n height: metadata.height ?? null,\n }\n } catch (error) {\n console.error('Error extracting image dimensions:', error)\n return { width: null, height: null }\n }\n}\n\n/**\n * Resize an image buffer to the specified dimensions\n * @param buffer - The image buffer to resize\n * @param width - Target width (optional)\n * @param height - Target height (optional)\n * @param format - Output format ('png', 'jpeg', 'webp'), auto-detected if not provided\n */\nexport async function resizeImage(\n buffer: Buffer,\n width?: number,\n height?: number,\n format?: 'png' | 'jpeg' | 'webp',\n): Promise<Buffer> {\n let pipeline = sharp(buffer)\n\n // Apply resize if dimensions provided\n if (width || height) {\n pipeline = pipeline.resize({\n width,\n height,\n fit: 'inside',\n withoutEnlargement: true,\n })\n }\n\n // Apply format conversion if specified\n if (format === 'png') {\n pipeline = pipeline.png()\n } else if (format === 'webp') {\n pipeline = pipeline.webp()\n } else if (format === 'jpeg') {\n pipeline = pipeline.jpeg()\n }\n\n return pipeline.toBuffer()\n}\n\n/**\n * Generate SHA-256 checksum for a buffer\n */\nexport function generateChecksum(buffer: Buffer): string {\n return createHash('sha256').update(buffer).digest('hex')\n}\n\n/**\n * Detect image format from mime type\n */\nexport function getImageFormat(mimeType: string): 'png' | 'webp' | 'jpeg' | null {\n if (mimeType.includes('png')) return 'png'\n if (mimeType.includes('webp')) return 'webp'\n if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'jpeg'\n return null\n}\n\n/**\n * Check if a mime type represents a raster image (not SVG)\n */\nexport function isRasterImage(mimeType: string): boolean {\n return mimeType.startsWith('image/') && !mimeType.includes('svg')\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\nimport { generateChecksum, getImageDimensions } from '../assets/assetUtils'\n\nexport function createIconRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAsset.findMany({\n where: { assetType: 'icon' },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n createdAt: true,\n updatedAt: true,\n },\n orderBy: { name: 'asc' },\n })\n }),\n\n getOne: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'icon',\n },\n })\n }),\n\n create: publicProcedure\n .input(\n z.object({\n name: z.string().min(1),\n content: z.string(), // base64 encoded binary\n mimeType: z.string(),\n fileSize: z.number().int().positive(),\n }),\n )\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n // Convert base64 to Buffer\n const buffer = Buffer.from(input.content, 'base64')\n \n // Generate checksum and extract dimensions\n const checksum = generateChecksum(buffer)\n const { width, height } = await getImageDimensions(buffer)\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n // Return existing asset if content is identical\n return existing\n }\n\n return prisma.dbAsset.create({\n data: {\n name: input.name,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n })\n }),\n\n update: publicProcedure\n .input(\n z.object({\n id: z.string(),\n name: z.string().min(1).optional(),\n content: z.string().optional(), // base64 encoded binary\n mimeType: z.string().optional(),\n fileSize: z.number().int().positive().optional(),\n }),\n )\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { id, content, ...rest } = input\n\n const data: Record<string, unknown> = { ...rest }\n \n if (content) {\n const buffer = Buffer.from(content, 'base64')\n data.content = new Uint8Array(buffer)\n data.checksum = generateChecksum(buffer)\n \n const { width, height } = await getImageDimensions(buffer)\n data.width = width\n data.height = height\n }\n\n return prisma.dbAsset.update({\n where: { id },\n data,\n })\n }),\n\n delete: publicProcedure\n .input(z.object({ id: z.string() }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.delete({\n where: { id: input.id },\n })\n }),\n\n deleteMany: publicProcedure\n .input(z.object({ ids: z.array(z.string()) }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.deleteMany({\n where: {\n id: { in: input.ids },\n assetType: 'icon',\n },\n })\n }),\n\n // Serve icon binary content\n getContent: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'icon',\n },\n select: { content: true, mimeType: true },\n })\n if (!asset) {\n throw new Error('Icon not found')\n }\n // Return base64 encoded content\n return {\n content: Buffer.from(asset.content).toString('base64'),\n mimeType: asset.mimeType,\n }\n }),\n })\n}\n","import { initTRPC } from '@trpc/server'\nimport z from 'zod'\n\nimport { getAppCatalogData } from '../modules/appCatalog/service'\nimport type { AppCatalogData, BootstrapConfigData, ResourceJumpsData } from '../types'\n\nimport type { TRPCRootObject } from '@trpc/server'\n\nimport { createAppCatalogAdminRouter } from '../modules/appCatalogAdmin/appCatalogAdminRouter.js'\nimport { createScreenshotRouter } from '../modules/assets/screenshotRouter.js'\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createIconRouter } from '../modules/icons/iconRouter.js'\nimport type { EhTrpcContext } from './ehTrpcContext'\n\n/**\n * Initialization of tRPC backend\n * Should be done only once per backend!\n */\nconst t: TRPCRootObject<EhTrpcContext, {}, {}> = initTRPC\n .context<EhTrpcContext>()\n .create({\n errorFormatter({ error, shape }: { error: unknown; shape: unknown }) {\n // Log all tRPC errors to console\n console.error('[tRPC Error]', {\n path: (shape as { data?: { path?: string } }).data?.path,\n code: (error as { code?: string }).code,\n message: (error as { message?: string }).message,\n cause: (error as { cause?: unknown }).cause,\n stack: (error as { stack?: string }).stack,\n })\n return shape\n },\n })\n\n/**\n * Export reusable router and procedure helpers\n * that can be used throughout the router\n */\nconst router: typeof t.router = t.router\nconst publicProcedure: typeof t.procedure = t.procedure\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(auth?: BetterAuth) {\n return router({\n bootstrap: publicProcedure.query(\n async ({ ctx }): Promise<BootstrapConfigData> => {\n return await ctx.companySpecificBackend.getBootstrapData()\n },\n ),\n\n availabilityMatrix: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getAvailabilityMatrix()\n }),\n\n tryFindRenameRule: publicProcedure\n .input(\n z.object({\n envSlug: z.string().optional(),\n resourceSlug: z.string().optional(),\n }),\n )\n .query(async ({ input, ctx }) => {\n return await ctx.companySpecificBackend.getNameMigrations(input)\n }),\n\n resourceJumps: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getResourceJumps()\n }),\n\n resourceJumpsExtended: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getResourceJumpsExtended()\n }),\n resourceJumpBySlugAndEnv: publicProcedure\n .input(\n z.object({\n jumpResourceSlug: z.string(),\n envSlug: z.string(),\n }),\n )\n .query(async ({ input, ctx }) => {\n return filterSingleResourceJump(\n await ctx.companySpecificBackend.getResourceJumps(),\n input.jumpResourceSlug,\n input.envSlug,\n )\n }),\n\n appCatalog: publicProcedure.query(async ({ ctx }): Promise<AppCatalogData> => {\n return await getAppCatalogData(ctx.companySpecificBackend.getApps)\n }),\n\n // Icon management routes\n icon: createIconRouter(t),\n\n // Screenshot management routes\n screenshot: createScreenshotRouter(t),\n\n // App catalog admin routes\n appCatalogAdmin: createAppCatalogAdminRouter(t),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth),\n })\n}\n\nfunction filterSingleResourceJump(\n resourceJumps: ResourceJumpsData,\n jumpResourceSlug: string,\n envSlug: string,\n): ResourceJumpsData {\n const filteredResourceJump = resourceJumps.resourceJumps.find(\n (item) => item.slug === jumpResourceSlug,\n )\n const filteredEnv = resourceJumps.envs.find((item) => item.slug === envSlug)\n\n return {\n resourceJumps: filteredResourceJump ? [filteredResourceJump] : [],\n envs: filteredEnv ? [filteredEnv] : [],\n lateResolvableParams: resourceJumps.lateResolvableParams,\n }\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n","import type { EhBackendCompanySpecificBackend } from '../types'\n\nexport interface EhTrpcContext {\n companySpecificBackend: EhBackendCompanySpecificBackend\n}\n\nexport interface EhTrpcContextOptions {\n companySpecificBackend: EhBackendCompanySpecificBackend\n}\n\nexport function createEhTrpcContext({\n companySpecificBackend,\n}: EhTrpcContextOptions): EhTrpcContext {\n return {\n companySpecificBackend,\n }\n}\n","export interface EhStaticControllerContract {\n methods: { \n getIcon: { method: string; url: string }\n getScreenshot: { method: string; url: string }\n }\n}\n\nexport const staticControllerContract: EhStaticControllerContract = {\n methods: {\n getIcon: {\n method: 'get',\n url: 'icon/:icon',\n },\n getScreenshot: {\n method: 'get',\n url: 'screenshot/:id',\n },\n },\n}\n","import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'\nimport { betterAuth } from 'better-auth'\nimport { prismaAdapter } from 'better-auth/adapters/prisma'\nimport { getDbClient } from '../../db'\n\nexport interface AuthConfig {\n appName?: string\n baseURL: string\n secret: string\n providers?: BetterAuthOptions['socialProviders']\n plugins?: Array<BetterAuthPlugin>\n /** Session expiration in seconds. Default: 7 days (604800) */\n sessionExpiresIn?: number\n /** Session update age in seconds. Default: 1 day (86400) */\n sessionUpdateAge?: number\n}\n\nexport function createAuth(config: AuthConfig) {\n const prisma = getDbClient()\n const isProduction = process.env.NODE_ENV === 'production'\n\n const auth = betterAuth({\n appName: config.appName || 'EnvHopper',\n baseURL: config.baseURL,\n basePath: '/api/auth',\n secret: config.secret,\n database: prismaAdapter(prisma, {\n provider: 'postgresql',\n }),\n socialProviders: config.providers || {},\n plugins: config.plugins || [],\n emailAndPassword: {\n enabled: true,\n },\n session: {\n expiresIn: config.sessionExpiresIn ?? 60 * 60 * 24 * 30,\n updateAge: config.sessionUpdateAge ?? 60 * 60 * 24,\n cookieCache: {\n enabled: true,\n maxAge: 300,\n },\n },\n advanced: {\n useSecureCookies: isProduction,\n },\n })\n\n return auth\n}\n\nexport type BetterAuth = ReturnType<typeof createAuth>\n","import { toNodeHandler } from 'better-auth/node'\nimport type { Express, Request, Response } from 'express'\nimport type { BetterAuth } from './auth'\n\n/**\n * Register Better Auth routes with Express\n * @param app - Express application instance\n * @param auth - Better Auth instance\n */\nexport function registerAuthRoutes(app: Express, auth: BetterAuth) {\n // Explicit session endpoint handler\n // Better Auth's toNodeHandler doesn't expose a direct /session endpoint\n app.get('/api/auth/session', async (req: Request, res: Response) => {\n try {\n const session = await auth.api.getSession({\n headers: req.headers as HeadersInit,\n })\n if (session) {\n res.json(session)\n } else {\n res.status(401).json({ error: 'Not authenticated' })\n }\n } catch (error) {\n console.error('[Auth Session Error]', error)\n res.status(500).json({ error: 'Internal server error' })\n }\n })\n\n // Use toNodeHandler to adapt better-auth for Express/Node.js\n // Express v5 wildcard syntax: /{*any} (also works with Express v4)\n const authHandler = toNodeHandler(auth)\n app.all('/api/auth/{*any}', authHandler)\n}\n","/**\n * Auth provider configuration from environment variables\n * This is the recommended way to configure auth providers.\n *\n * Supports: GitHub, Google via environment variables\n * For Okta and other custom providers, use getAuthPluginsFromEnv()\n *\n * Example .env:\n * AUTH_GITHUB_CLIENT_ID=your_github_client_id\n * AUTH_GITHUB_CLIENT_SECRET=your_github_client_secret\n * AUTH_GOOGLE_CLIENT_ID=your_google_client_id\n * AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret\n */\n\nimport type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'\nimport { genericOAuth, okta } from 'better-auth/plugins'\n\nexport function getAuthProvidersFromEnv(): BetterAuthOptions['socialProviders'] {\n const providers: BetterAuthOptions['socialProviders'] = {}\n\n // GitHub OAuth\n if (\n process.env.AUTH_GITHUB_CLIENT_ID &&\n process.env.AUTH_GITHUB_CLIENT_SECRET\n ) {\n providers.github = {\n clientId: process.env.AUTH_GITHUB_CLIENT_ID,\n clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET,\n }\n }\n\n // Google OAuth\n if (\n process.env.AUTH_GOOGLE_CLIENT_ID &&\n process.env.AUTH_GOOGLE_CLIENT_SECRET\n ) {\n providers.google = {\n clientId: process.env.AUTH_GOOGLE_CLIENT_ID,\n clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,\n }\n }\n\n return providers\n}\n\n/**\n * Get auth plugins from environment variables\n * Currently supports: Okta\n *\n * Example .env:\n * AUTH_OKTA_CLIENT_ID=your_okta_client_id\n * AUTH_OKTA_CLIENT_SECRET=your_okta_client_secret\n * AUTH_OKTA_ISSUER=https://your-org.okta.com/oauth2/ausxb83g4wY1x09ec0h7\n *\n * Note: If you get \"User is not assigned to the client application\" errors,\n * you need to configure your Okta application to allow all users:\n * 1. In Okta Admin Console, go to Applications → Your App\n * 2. Assignments tab → Assign to Groups → Add \"Everyone\" group\n * OR\n * 3. Edit the application → In \"User consent\" section, enable appropriate settings\n *\n * For group-based authorization:\n * 1. Add \"groups\" scope to your auth server policy rule\n * 2. Create a groups claim in your auth server\n * 3. Groups will be available in the user object after authentication\n */\nexport function getAuthPluginsFromEnv(): Array<BetterAuthPlugin> {\n const plugins: Array<BetterAuthPlugin> = []\n const oktaConfig: Array<ReturnType<typeof okta>> = []\n\n if (\n process.env.AUTH_OKTA_CLIENT_ID &&\n process.env.AUTH_OKTA_CLIENT_SECRET &&\n process.env.AUTH_OKTA_ISSUER\n ) {\n oktaConfig.push(\n okta({\n clientId: process.env.AUTH_OKTA_CLIENT_ID,\n clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET,\n issuer: process.env.AUTH_OKTA_ISSUER,\n }),\n )\n }\n\n if (oktaConfig.length > 0) {\n plugins.push(genericOAuth({ config: oktaConfig }))\n }\n\n return plugins\n}\n\n/**\n * Validate required auth environment variables\n */\nexport function validateAuthConfig(): void {\n const secret = process.env.BETTER_AUTH_SECRET\n const baseUrl = process.env.BETTER_AUTH_URL\n\n if (!secret) {\n console.warn(\n 'BETTER_AUTH_SECRET not set. Using development fallback. Set this in production!',\n )\n }\n\n if (!baseUrl) {\n console.info('BETTER_AUTH_URL not set. Using default http://localhost:3000')\n }\n}\n","/**\n * Authorization utilities for checking user permissions based on groups\n *\n * Groups are automatically included in the user session when:\n * 1. Okta auth server has a \"groups\" claim configured\n * 2. The auth policy rule includes \"groups\" in scope_whitelist\n *\n * Example usage in tRPC procedures:\n * ```typescript\n * myProcedure: protectedProcedure.query(async ({ ctx }) => {\n * if (requireAdmin(ctx.user)) {\n * // Admin-only logic\n * }\n * // Regular user logic\n * })\n * ```\n */\n\nexport interface UserWithGroups {\n id: string\n email: string\n name?: string\n // Groups from Okta (or other identity provider)\n // This will be populated if groups claim is configured\n [key: string]: any\n}\n\n/**\n * Extract groups from user object\n * Groups can be stored in different locations depending on the OAuth provider\n */\nexport function getUserGroups(\n user: UserWithGroups | null | undefined,\n): Array<string> {\n if (!user) {\n return []\n }\n\n // Check common locations for group information\n const groups =\n user.groups || // Standard \"groups\" claim\n (user as any).env_hopper_groups || // Custom env_hopper_groups claim\n (user as any).oktaGroups || // Okta-specific\n (user as any).roles || // Some providers use \"roles\"\n []\n\n return Array.isArray(groups) ? groups : []\n}\n\n/**\n * Check if user is a member of any of the specified groups\n */\nexport function isMemberOfAnyGroup(\n user: UserWithGroups | null | undefined,\n allowedGroups: Array<string>,\n): boolean {\n const userGroups = getUserGroups(user)\n return allowedGroups.some((group) => userGroups.includes(group))\n}\n\n/**\n * Check if user is a member of all specified groups\n */\nexport function isMemberOfAllGroups(\n user: UserWithGroups | null | undefined,\n requiredGroups: Array<string>,\n): boolean {\n const userGroups = getUserGroups(user)\n return requiredGroups.every((group) => userGroups.includes(group))\n}\n\n/**\n * Get admin group names from environment variables\n * Default: env_hopper_ui_super_admins\n */\nexport function getAdminGroupsFromEnv(): Array<string> {\n const adminGroups =\n process.env.AUTH_ADMIN_GROUPS || 'env_hopper_ui_super_admins'\n return adminGroups\n .split(',')\n .map((g) => g.trim())\n .filter(Boolean)\n}\n\n/**\n * Check if user has admin permissions\n */\nexport function isAdmin(user: UserWithGroups | null | undefined): boolean {\n const adminGroups = getAdminGroupsFromEnv()\n return isMemberOfAnyGroup(user, adminGroups)\n}\n\n/**\n * Require admin permissions - throws error if not admin\n */\nexport function requireAdmin(user: UserWithGroups | null | undefined): void {\n if (!isAdmin(user)) {\n throw new Error('Forbidden: Admin access required')\n }\n}\n\n/**\n * Require membership in specific groups - throws error if not member\n */\nexport function requireGroups(\n user: UserWithGroups | null | undefined,\n groups: Array<string>,\n): void {\n if (!isMemberOfAnyGroup(user, groups)) {\n throw new Error(\n `Forbidden: Membership in one of these groups required: ${groups.join(', ')}`,\n )\n }\n}\n","import { stepCountIs, streamText, tool } from 'ai'\nimport type { LanguageModel, Tool } from 'ai'\n\nimport type { Request, Response } from 'express'\n\nexport interface AdminChatHandlerOptions {\n /** The AI model to use (from @ai-sdk/openai, @ai-sdk/anthropic, etc.) */\n model: LanguageModel\n /** System prompt for the AI assistant */\n systemPrompt?: string\n /** Tools available to the AI assistant */\n tools?: Record<string, Tool>\n /**\n * Optional function to validate configuration before processing requests.\n * Should throw an error if configuration is invalid (e.g., missing API key).\n * @example\n * validateConfig: () => {\n * if (!process.env.OPENAI_API_KEY) {\n * throw new Error('OPENAI_API_KEY is not configured')\n * }\n * }\n */\n validateConfig?: () => void\n}\n\ninterface TextPart {\n type: 'text'\n text: string\n}\n\ninterface UIMessageInput {\n role: 'user' | 'assistant' | 'system'\n content?: string\n parts?: Array<TextPart | { type: string }>\n}\n\ninterface CoreMessage {\n role: 'user' | 'assistant' | 'system'\n content: string\n}\n\nfunction convertToCoreMessages(\n messages: Array<UIMessageInput>,\n): Array<CoreMessage> {\n return messages.map((msg) => {\n if (msg.content) {\n return { role: msg.role, content: msg.content }\n }\n // Extract text from parts array (AI SDK v3 format)\n const textContent =\n msg.parts\n ?.filter((part): part is TextPart => part.type === 'text')\n .map((part) => part.text)\n .join('') ?? ''\n return { role: msg.role, content: textContent }\n })\n}\n\n/**\n * Creates an Express handler for the admin chat endpoint.\n *\n * Usage in thin wrappers:\n *\n * ```typescript\n * // With OpenAI\n * import { openai } from '@ai-sdk/openai'\n * app.post('/api/admin/chat', createAdminChatHandler({\n * model: openai('gpt-4o-mini'),\n * }))\n *\n * // With Claude\n * import { anthropic } from '@ai-sdk/anthropic'\n * app.post('/api/admin/chat', createAdminChatHandler({\n * model: anthropic('claude-sonnet-4-20250514'),\n * }))\n * ```\n */\nexport function createAdminChatHandler(options: AdminChatHandlerOptions) {\n const {\n model,\n systemPrompt = 'You are a helpful admin assistant for the Env Hopper application. Help users manage apps, data sources, and MCP server configurations.',\n tools = {},\n validateConfig,\n } = options\n\n return async (req: Request, res: Response) => {\n try {\n // Validate configuration if validator provided\n if (validateConfig) {\n validateConfig()\n }\n\n const { messages } = req.body as { messages: Array<UIMessageInput> }\n const coreMessages = convertToCoreMessages(messages)\n\n console.log(\n '[Admin Chat] Received messages:',\n JSON.stringify(coreMessages, null, 2),\n )\n console.log('[Admin Chat] Available tools:', Object.keys(tools))\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n tools,\n // Allow up to 5 steps so the model can call tools and then generate a response\n stopWhen: stepCountIs(5),\n onFinish: (event) => {\n console.log('[Admin Chat] Finished:', {\n finishReason: event.finishReason,\n usage: event.usage,\n hasText: !!event.text,\n textLength: event.text.length,\n })\n },\n })\n\n // Use UI message stream response which is compatible with AI SDK React hooks\n const response = result.toUIMessageStreamResponse()\n\n // Copy headers from the response\n response.headers.forEach((value, key) => {\n res.setHeader(key, value)\n })\n\n // Pipe the stream to the response\n if (response.body) {\n const reader = response.body.getReader()\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read()\n if (done) {\n res.end()\n return\n }\n res.write(value)\n return pump()\n }\n await pump()\n } else {\n console.error('[Admin Chat] No response body')\n res.status(500).json({ error: 'No response from AI model' })\n }\n } catch (error) {\n console.error('[Admin Chat] Error:', error)\n res.status(500).json({ error: 'Failed to process chat request' })\n }\n }\n}\n\n// Re-export tool helper for convenience\nexport { tool }\n","import type { Tool } from 'ai'\nimport { z } from 'zod'\nimport { getDbClient } from '../../../db'\n\n/**\n * Generic interface for executing raw SQL queries.\n * Can be implemented with Prisma's $queryRawUnsafe or any other SQL client.\n */\nexport interface DatabaseClient {\n /** Execute a SELECT query and return results */\n query: <T = unknown>(sql: string) => Promise<Array<T>>\n /** Execute an INSERT/UPDATE/DELETE and return affected row count */\n execute: (sql: string) => Promise<{ affectedRows: number }>\n /** Get list of tables in the database */\n getTables: () => Promise<Array<string>>\n /** Get columns for a specific table */\n getColumns: (\n tableName: string,\n ) => Promise<Array<{ name: string; type: string; nullable: boolean }>>\n}\n\n/**\n * Creates a DatabaseClient from a Prisma client.\n */\nexport function createPrismaDatabaseClient(prisma: {\n $queryRawUnsafe: <T>(sql: string) => Promise<T>\n $executeRawUnsafe: (sql: string) => Promise<number>\n}): DatabaseClient {\n return {\n query: async <T = unknown>(sql: string): Promise<Array<T>> => {\n const result = await prisma.$queryRawUnsafe<Array<T>>(sql)\n return result\n },\n execute: async (sql: string) => {\n const affectedRows = await prisma.$executeRawUnsafe(sql)\n return { affectedRows }\n },\n getTables: async () => {\n const tables = await prisma.$queryRawUnsafe<Array<{ tablename: string }>>(\n `SELECT tablename FROM pg_tables WHERE schemaname = 'public'`,\n )\n return tables.map((t) => t.tablename)\n },\n getColumns: async (tableName: string) => {\n const columns = await prisma.$queryRawUnsafe<\n Array<{\n column_name: string\n data_type: string\n is_nullable: string\n }>\n >(\n `SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}' AND table_schema = 'public'`,\n )\n return columns.map((c) => ({\n name: c.column_name,\n type: c.data_type,\n nullable: c.is_nullable === 'YES',\n }))\n },\n }\n}\n\n// Define zod schemas for tool parameters\nconst querySchema = z.object({\n sql: z.string().describe('The SELECT SQL query to execute'),\n})\n\nconst modifySchema = z.object({\n sql: z\n .string()\n .describe('The INSERT, UPDATE, or DELETE SQL query to execute'),\n confirmed: z\n .boolean()\n .describe('Must be true to execute destructive operations'),\n})\n\nconst schemaParamsSchema = z.object({\n tableName: z\n .string()\n .optional()\n .describe(\n 'Specific table name to get columns for. If not provided, returns list of all tables.',\n ),\n})\n\ntype QueryInput = z.infer<typeof querySchema>\ntype ModifyInput = z.infer<typeof modifySchema>\ntype SchemaInput = z.infer<typeof schemaParamsSchema>\n\n/**\n * Creates a DatabaseClient using the internal backend-core Prisma client.\n * This is a convenience function for apps that don't need to pass their own Prisma client.\n */\nfunction createInternalDatabaseClient(): DatabaseClient {\n return createPrismaDatabaseClient(getDbClient())\n}\n\n/**\n * Creates AI tools for generic database access.\n *\n * The AI uses these internally - users interact via natural language.\n * Results are formatted as tables by the AI based on the system prompt.\n * Uses the internal backend-core Prisma client automatically.\n */\nexport function createDatabaseTools(): Record<string, Tool> {\n const db = createInternalDatabaseClient()\n const queryDatabase: Tool<QueryInput, unknown> = {\n description: `Execute a SELECT query to read data from the database.\nUse this to list, search, or filter records from any table.\nAlways use double quotes around table and column names for PostgreSQL (e.g., SELECT * FROM \"App\").\nReturn results will be formatted as a table for the user.`,\n inputSchema: querySchema,\n execute: async ({ sql }) => {\n console.log(`Executing ${sql}`)\n // Safety check - only allow SELECT\n const normalizedSql = sql.trim().toUpperCase()\n if (!normalizedSql.startsWith('SELECT')) {\n return {\n error:\n 'Only SELECT queries are allowed with queryDatabase. Use modifyDatabase for changes.',\n }\n }\n try {\n const results = await db.query(sql)\n return {\n success: true,\n rowCount: Array.isArray(results) ? results.length : 0,\n data: results,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Query failed',\n }\n }\n },\n }\n\n const modifyDatabase: Tool<ModifyInput, unknown> = {\n description: `Execute an INSERT, UPDATE, or DELETE query to modify data.\nUse double quotes around table and column names for PostgreSQL.\nIMPORTANT: Always ask for user confirmation before executing. Set confirmed=true only after user confirms.\nFor UPDATE/DELETE, always include a WHERE clause to avoid affecting all rows.`,\n inputSchema: modifySchema,\n execute: async ({ sql, confirmed }) => {\n if (!confirmed) {\n return {\n needsConfirmation: true,\n message: 'Please confirm you want to execute this operation.',\n sql,\n }\n }\n\n // Safety check - don't allow SELECT here\n const normalizedSql = sql.trim().toUpperCase()\n if (normalizedSql.startsWith('SELECT')) {\n return { error: 'Use queryDatabase for SELECT queries.' }\n }\n\n // Extra safety - warn about missing WHERE on UPDATE/DELETE\n if (\n (normalizedSql.startsWith('UPDATE') ||\n normalizedSql.startsWith('DELETE')) &&\n !normalizedSql.includes('WHERE')\n ) {\n return {\n error:\n 'UPDATE and DELETE queries must include a WHERE clause for safety.',\n sql,\n }\n }\n\n try {\n const result = await db.execute(sql)\n return {\n success: true,\n affectedRows: result.affectedRows,\n message: `Operation completed. ${result.affectedRows} row(s) affected.`,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Operation failed',\n }\n }\n },\n }\n\n const getDatabaseSchema: Tool<SchemaInput, unknown> = {\n description: `Get information about database tables and their columns.\nUse this to understand the database structure before writing queries.\nCall without tableName to list all tables, or with tableName to get columns for a specific table.`,\n inputSchema: schemaParamsSchema,\n execute: async ({ tableName }) => {\n try {\n if (tableName) {\n const columns = await db.getColumns(tableName)\n return {\n success: true,\n table: tableName,\n columns,\n }\n } else {\n const tables = await db.getTables()\n return {\n success: true,\n tables,\n }\n }\n } catch (error) {\n return {\n success: false,\n error:\n error instanceof Error ? error.message : 'Failed to get schema',\n }\n }\n },\n }\n\n return {\n queryDatabase,\n modifyDatabase,\n getDatabaseSchema,\n }\n}\n\n/**\n * Default system prompt for the database admin assistant.\n * Can be customized or extended.\n */\nexport const DEFAULT_ADMIN_SYSTEM_PROMPT = `You are a helpful database admin assistant. You help users view and manage data in the database.\n\nIMPORTANT RULES:\n1. When showing data, ALWAYS format it as a numbered ASCII table so users can reference rows by number\n2. NEVER show raw SQL to users - just describe what you're doing in plain language\n3. When users ask to modify data (update, delete, create), ALWAYS confirm before executing\n4. For updates, show the current value and ask for confirmation before changing\n5. Keep responses concise and focused on the data\n\nFORMATTING EXAMPLE:\nWhen user asks \"show me all apps\", respond like:\n\"Here are all the apps:\n\n| # | ID | Slug | Display Name | Icon |\n|---|----|---------|-----------------| -------|\n| 1 | 1 | portal | Portal | portal |\n| 2 | 2 | admin | Admin Dashboard | admin |\n| 3 | 3 | api | API Service | null |\n\nFound 3 apps total.\"\n\nWhen user says \"update row 2 display name to 'Admin Panel'\":\n1. First confirm: \"I'll update the app 'Admin Dashboard' (ID: 2) to have display name 'Admin Panel'. Proceed?\"\n2. Only after user confirms, execute the update\n3. Then show the updated row\n\nAVAILABLE TABLES:\nUse getDatabaseSchema tool to discover tables and their columns.\n`\n","import type { Request, Response, Router } from 'express'\nimport multer from 'multer'\nimport { createHash } from 'node:crypto'\nimport { getDbClient } from '../../db'\n\n// Configure multer for memory storage\nconst upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: 10 * 1024 * 1024, // 10MB limit\n },\n fileFilter: (_req, file, cb) => {\n // Accept images only\n if (!file.mimetype.startsWith('image/')) {\n cb(new Error('Only image files are allowed'))\n return\n }\n cb(null, true)\n },\n})\n\nexport interface IconRestControllerConfig {\n /**\n * Base path for icon endpoints (e.g., '/api/icons')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for icon upload and retrieval\n *\n * Endpoints:\n * - POST {basePath}/upload - Upload a new icon (multipart/form-data with 'icon' field and 'name' field)\n * - GET {basePath}/:id - Get icon binary by ID\n * - GET {basePath}/:id/metadata - Get icon metadata only\n */\nexport function registerIconRestController(\n router: Router,\n config: IconRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Upload endpoint - accepts multipart/form-data\n router.post(\n `${basePath}/upload`,\n upload.single('icon'),\n async (req: Request, res: Response) => {\n try {\n if (!req.file) {\n res.status(400).json({ error: 'No file uploaded' })\n return\n }\n\n const name = req.body['name'] as string\n if (!name) {\n res.status(400).json({ error: 'Name is required' })\n return\n }\n\n const prisma = getDbClient()\n const checksum = createHash('sha256')\n .update(req.file.buffer)\n .digest('hex')\n const icon = await prisma.dbAsset.create({\n data: {\n name,\n assetType: 'icon',\n content: new Uint8Array(req.file.buffer),\n mimeType: req.file.mimetype,\n fileSize: req.file.size,\n checksum,\n },\n })\n\n res.status(201).json({\n id: icon.id,\n name: icon.name,\n mimeType: icon.mimeType,\n fileSize: icon.fileSize,\n createdAt: icon.createdAt,\n })\n } catch (error) {\n console.error('Error uploading icon:', error)\n res.status(500).json({ error: 'Failed to upload icon' })\n }\n },\n )\n\n // Get icon binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', icon.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${icon.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(icon.content)\n } catch (error) {\n console.error('Error fetching icon:', error)\n res.status(500).json({ error: 'Failed to fetch icon' })\n }\n })\n\n // Get icon metadata only (no binary content)\n router.get(\n `${basePath}/:id/metadata`,\n async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n res.json(icon)\n } catch (error) {\n console.error('Error fetching icon metadata:', error)\n res.status(500).json({ error: 'Failed to fetch icon metadata' })\n }\n },\n )\n\n // Get icon binary by name\n router.get(\n `${basePath}/by-name/:name`,\n async (req: Request, res: Response) => {\n try {\n const { name } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', icon.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${icon.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(icon.content)\n } catch (error) {\n console.error('Error fetching icon by name:', error)\n res.status(500).json({ error: 'Failed to fetch icon' })\n }\n },\n )\n}\n","import { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from '../assets/assetUtils'\n\nexport interface UpsertIconInput {\n name: string\n content: Buffer\n mimeType: string\n fileSize: number\n}\n\n/**\n * Upsert an icon to the database.\n * If an icon with the same name exists, it will be updated.\n * Otherwise, a new icon will be created.\n */\nexport async function upsertIcon(input: UpsertIconInput) {\n const prisma = getDbClient()\n \n const checksum = generateChecksum(input.content)\n const { width, height } = await getImageDimensions(input.content)\n \n return prisma.dbAsset.upsert({\n where: { name: input.name },\n update: {\n content: new Uint8Array(input.content),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n create: {\n name: input.name,\n assetType: 'icon',\n content: new Uint8Array(input.content),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n })\n}\n\n/**\n * Upsert multiple icons to the database.\n * This is more efficient than calling upsertIcon multiple times.\n */\nexport async function upsertIcons(icons: Array<UpsertIconInput>) {\n const results = []\n for (const icon of icons) {\n const result = await upsertIcon(icon)\n results.push(result)\n }\n return results\n}\n\n/**\n * Get an asset (icon or screenshot) by name from the database.\n * Returns the asset content, mimeType, and name if found.\n */\nexport async function getAssetByName(name: string) {\n const prisma = getDbClient()\n \n return prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n}\n","import type { AssetType } from '@prisma/client'\nimport type { Request, Response, Router } from 'express'\nimport multer from 'multer'\nimport sharp from 'sharp'\nimport { getDbClient } from '../../db'\nimport {\n generateChecksum,\n getImageDimensions,\n getImageFormat,\n isRasterImage,\n resizeImage,\n} from './assetUtils'\n\n// Configure multer for memory storage\nconst upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: 10 * 1024 * 1024, // 10MB limit\n },\n fileFilter: (_req, file, cb) => {\n // Accept images only\n if (!file.mimetype.startsWith('image/')) {\n cb(new Error('Only image files are allowed'))\n return\n }\n cb(null, true)\n },\n})\n\nexport interface AssetRestControllerConfig {\n /**\n * Base path for asset endpoints (e.g., '/api/assets')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for universal asset upload and retrieval\n *\n * Endpoints:\n * - POST {basePath}/upload - Upload a new asset (multipart/form-data)\n * - GET {basePath}/:id - Get asset binary by ID\n * - GET {basePath}/:id/metadata - Get asset metadata only\n * - GET {basePath}/by-name/:name - Get asset binary by name\n */\nexport function registerAssetRestController(\n router: Router,\n config: AssetRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Upload endpoint - accepts multipart/form-data\n router.post(\n `${basePath}/upload`,\n upload.single('asset'),\n async (req: Request, res: Response) => {\n try {\n if (!req.file) {\n res.status(400).json({ error: 'No file uploaded' })\n return\n }\n\n const name = req.body['name'] as string\n const assetTypeInput = req.body['assetType']\n const assetType = (assetTypeInput as AssetType | undefined) ?? 'icon'\n\n if (!name) {\n res.status(400).json({ error: 'Name is required' })\n return\n }\n\n const prisma = getDbClient()\n\n // Compute checksum of the binary for content-based deduplication.\n const checksum = generateChecksum(req.file.buffer)\n\n // If an asset with the same checksum already exists, reuse it instead of storing duplicate binary.\n const existing = await prisma.dbAsset.findUnique({\n where: { checksum },\n select: {\n id: true,\n name: true,\n assetType: true,\n checksum: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n if (existing) {\n res.status(200).json(existing)\n return\n }\n\n // Get image dimensions using our utility\n const { width, height } = await getImageDimensions(req.file.buffer)\n\n const asset = await prisma.dbAsset.create({\n data: {\n name,\n checksum,\n assetType,\n content: new Uint8Array(req.file.buffer),\n mimeType: req.file.mimetype,\n fileSize: req.file.size,\n width,\n height,\n },\n })\n\n res.status(201).json({\n id: asset.id,\n name: asset.name,\n assetType: asset.assetType,\n mimeType: asset.mimeType,\n fileSize: asset.fileSize,\n width: asset.width,\n height: asset.height,\n createdAt: asset.createdAt,\n })\n } catch (error) {\n console.error('Error uploading asset:', error)\n res.status(500).json({ error: 'Failed to upload asset' })\n }\n },\n )\n\n // Get asset binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n width: true,\n height: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n const resizeEnabled =\n String(process.env.EH_ASSETS_RESIZE_ENABLED || 'true') === 'true'\n const wParam = req.query['w'] as string | undefined\n const width = wParam ? Number.parseInt(wParam, 10) : undefined\n\n let outBuffer: Uint8Array = asset.content\n let outMime = asset.mimeType\n\n const shouldResize =\n resizeEnabled &&\n isRasterImage(asset.mimeType) &&\n !!width &&\n Number.isFinite(width) &&\n width > 0\n\n if (shouldResize) {\n const fmt = getImageFormat(asset.mimeType) || 'jpeg'\n const buf = await resizeImage(\n Buffer.from(asset.content),\n width,\n undefined,\n fmt,\n )\n outBuffer = new Uint8Array(buf)\n outMime = `image/${fmt}`\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', outMime)\n res.setHeader('Content-Disposition', `inline; filename=\"${asset.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content (resized if requested)\n res.send(outBuffer)\n } catch (error) {\n console.error('Error fetching asset:', error)\n res.status(500).json({ error: 'Failed to fetch asset' })\n }\n })\n\n // Get asset metadata only (no binary content)\n router.get(\n `${basePath}/:id/metadata`,\n async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n assetType: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n res.json(asset)\n } catch (error) {\n console.error('Error fetching asset metadata:', error)\n res.status(500).json({ error: 'Failed to fetch asset metadata' })\n }\n },\n )\n\n // Get asset binary by name\n router.get(\n `${basePath}/by-name/:name`,\n async (req: Request, res: Response) => {\n try {\n const { name } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n width: true,\n height: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n const resizeEnabled =\n String(process.env.EH_ASSETS_RESIZE_ENABLED || 'true') === 'true'\n const wParam = req.query['w'] as string | undefined\n const width = wParam ? Number.parseInt(wParam, 10) : undefined\n\n let outBuffer: Uint8Array = asset.content\n let outMime = asset.mimeType\n\n const isRaster =\n asset.mimeType.startsWith('image/') && !asset.mimeType.includes('svg')\n const shouldResize =\n resizeEnabled &&\n isRaster &&\n !!width &&\n Number.isFinite(width) &&\n width > 0\n\n if (shouldResize) {\n const fmt = asset.mimeType.includes('png')\n ? 'png'\n : asset.mimeType.includes('webp')\n ? 'webp'\n : 'jpeg'\n\n let buf: Buffer\n const pipeline = sharp(Buffer.from(asset.content)).resize({\n width,\n fit: 'inside',\n withoutEnlargement: true,\n })\n if (fmt === 'png') {\n buf = await pipeline.png().toBuffer()\n outMime = 'image/png'\n } else if (fmt === 'webp') {\n buf = await pipeline.webp().toBuffer()\n outMime = 'image/webp'\n } else {\n buf = await pipeline.jpeg().toBuffer()\n outMime = 'image/jpeg'\n }\n outBuffer = new Uint8Array(buf)\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', outMime)\n res.setHeader('Content-Disposition', `inline; filename=\"${asset.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content (resized if requested)\n res.send(outBuffer)\n } catch (error) {\n console.error('Error fetching asset by name:', error)\n res.status(500).json({ error: 'Failed to fetch asset' })\n }\n },\n )\n}\n","import type { Request, Response, Router } from 'express'\nimport sharp from 'sharp'\nimport { getDbClient } from '../../db'\n\nexport interface ScreenshotRestControllerConfig {\n /**\n * Base path for screenshot endpoints (e.g., '/api/screenshots')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for screenshot retrieval\n * \n * Endpoints:\n * - GET {basePath}/app/:appId - Get all screenshots for an app\n * - GET {basePath}/:id - Get screenshot binary by ID\n * - GET {basePath}/:id/metadata - Get screenshot metadata only\n */\nexport function registerScreenshotRestController(\n router: Router,\n config: ScreenshotRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Get all screenshots for an app\n router.get(`${basePath}/app/:appSlug`, async (req: Request, res: Response) => {\n try {\n const { appSlug } = req.params\n\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app) {\n res.status(404).json({ error: 'App not found' })\n return\n }\n\n // Fetch all screenshots for the app\n const screenshots = await prisma.dbAsset.findMany({\n where: {\n id: { in: app.screenshotIds },\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n res.json(screenshots)\n } catch (error) {\n console.error('Error fetching app screenshots:', error)\n res.status(500).json({ error: 'Failed to fetch screenshots' })\n }\n })\n\n // Get first screenshot for an app (convenience endpoint)\n router.get(`${basePath}/app/:appSlug/first`, async (req: Request, res: Response) => {\n try {\n const { appSlug } = req.params\n\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app || app.screenshotIds.length === 0) {\n res.status(404).json({ error: 'No screenshots found' })\n return\n }\n\n // Fetch first screenshot\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id: app.screenshotIds[0] },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n res.json(screenshot)\n } catch (error) {\n console.error('Error fetching first screenshot:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot' })\n }\n })\n\n // Get screenshot binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n const sizeParam = req.query.size as string | undefined\n const targetSize = sizeParam ? parseInt(sizeParam, 10) : undefined\n\n const prisma = getDbClient()\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n let content: Uint8Array | Buffer = screenshot.content\n\n // Resize if size parameter provided\n if (targetSize && targetSize > 0) {\n try {\n content = await sharp(screenshot.content)\n .resize(targetSize, targetSize, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n .toBuffer()\n } catch (resizeError) {\n console.error('Error resizing screenshot:', resizeError)\n // Fall back to original if resize fails\n }\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', screenshot.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${screenshot.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(content)\n } catch (error) {\n console.error('Error fetching screenshot:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot' })\n }\n })\n\n // Get screenshot metadata only (no binary content)\n router.get(`${basePath}/:id/metadata`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n res.json(screenshot)\n } catch (error) {\n console.error('Error fetching screenshot metadata:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot metadata' })\n }\n })\n}\n","import { readFileSync, readdirSync } from 'node:fs'\nimport { extname, join } from 'node:path'\nimport { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from './assetUtils'\n\nexport interface SyncAssetsConfig {\n /**\n * Directory containing icon files to sync\n */\n iconsDir?: string\n\n /**\n * Directory containing screenshot files to sync\n */\n screenshotsDir?: string\n}\n\n/**\n * Sync local asset files (icons and screenshots) from directories into the database.\n * \n * This function allows consuming applications to sync asset files without directly\n * exposing the Prisma client. It handles:\n * - Icon files: Assigned to apps by matching filename to icon name patterns\n * - Screenshot files: Assigned to apps by matching filename to app ID (format: <app-id>_screenshot_<no>.<ext>)\n * \n * @param config Configuration with paths to icon and screenshot directories\n */\nexport async function syncAssets(config: SyncAssetsConfig): Promise<{\n iconsUpserted: number\n screenshotsUpserted: number\n}> {\n const prisma = getDbClient()\n let iconsUpserted = 0\n let screenshotsUpserted = 0\n\n // Sync icons from local/icons directory\n if (config.iconsDir) {\n console.log(`📁 Syncing icons from ${config.iconsDir}...`)\n iconsUpserted = await syncIconsFromDirectory(prisma, config.iconsDir)\n console.log(` ✓ Upserted ${iconsUpserted} icons`)\n }\n\n // Sync screenshots from local/screenshots directory\n if (config.screenshotsDir) {\n console.log(`📷 Syncing screenshots from ${config.screenshotsDir}...`)\n screenshotsUpserted = await syncScreenshotsFromDirectory(prisma, config.screenshotsDir)\n console.log(` ✓ Upserted ${screenshotsUpserted} screenshots and assigned to apps`)\n }\n\n return {\n iconsUpserted,\n screenshotsUpserted,\n }\n}\n\n/**\n * Sync icon files from a directory\n */\nasync function syncIconsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n iconsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(iconsDir)\n\n for (const file of files) {\n const filePath = join(iconsDir, file)\n const ext = extname(file).toLowerCase().slice(1) // Remove leading dot\n\n // Skip non-image files\n if (!['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n continue\n }\n\n try {\n const content = readFileSync(filePath)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n const iconName = file.replace(/\\.[^/.]+$/, '') // Remove extension\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n continue // Already synced\n }\n\n // Extract dimensions for raster images\n let width: number | null = null\n let height: number | null = null\n if (!ext.includes('svg')) {\n const { width: w, height: h } = await getImageDimensions(buffer)\n width = w\n height = h\n }\n\n // Determine MIME type\n const mimeType = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n }[ext] || 'application/octet-stream'\n\n await prisma.dbAsset.create({\n data: {\n name: iconName,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync icon ${file}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading icons directory:`, error)\n }\n\n return count\n}\n\n/**\n * Sync screenshot files from a directory and assign to apps\n */\nasync function syncScreenshotsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n screenshotsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(screenshotsDir)\n\n // Group screenshots by app ID\n const screenshotsByApp = new Map<string, Array<{ path: string; ext: string }>>()\n\n for (const file of files) {\n // Parse filename: <app-id>_screenshot_<no>.<ext>\n const match = file.match(/^(.+?)_screenshot_(\\d+)\\.([^.]+)$/)\n if (!match || !match[1] || !match[3]) {\n continue\n }\n\n const appId = match[1]\n const ext = match[3]\n if (!screenshotsByApp.has(appId)) {\n screenshotsByApp.set(appId, [])\n }\n screenshotsByApp.get(appId)!.push({\n path: join(screenshotsDir, file),\n ext,\n })\n }\n\n // Process each app's screenshots\n for (const [appId, screenshots] of screenshotsByApp) {\n try {\n // Check if app exists\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n select: { id: true },\n })\n\n if (!app) {\n console.warn(` ⚠ App not found: ${appId}`)\n continue\n }\n\n // Sync screenshots for this app\n for (const screenshot of screenshots) {\n try {\n const content = readFileSync(screenshot.path)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n\n // Check if screenshot with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'screenshot' },\n })\n\n if (existing) {\n // Link to app via screenshotIds array if not already linked\n const existingApp = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n })\n if (existingApp && !existingApp.screenshotIds.includes(existing.id)) {\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: [...existingApp.screenshotIds, existing.id],\n },\n })\n }\n continue\n }\n\n // Extract dimensions\n const { width, height } = await getImageDimensions(buffer)\n\n // Determine MIME type\n const mimeType = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n }[screenshot.ext.toLowerCase()] || 'application/octet-stream'\n\n // Create screenshot asset\n const asset = await prisma.dbAsset.create({\n data: {\n name: `${appId}-screenshot-${Date.now()}`,\n assetType: 'screenshot',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n // Link screenshot to app via screenshotIds array\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: {\n push: asset.id,\n },\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync screenshot ${screenshot.path}:`, error)\n }\n }\n } catch (error) {\n console.warn(` ⚠ Failed to process app ${appId}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading screenshots directory:`, error)\n }\n\n return count\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["prismaClient: PrismaClient | null","categories: Array<AppCategory>","duplicateKeys: Array<string>","results: Array<MakeTFromPrismaModel<TPrismaModelName>>","router","t","publicProcedure","router","t","publicProcedure","router","t","publicProcedure","providers: Array<string>","router","t","publicProcedure","data: Record<string, unknown>","t: TRPCRootObject<EhTrpcContext, {}, {}>","router: typeof t.router","publicProcedure: typeof t.procedure","z","staticControllerContract: EhStaticControllerContract","providers: BetterAuthOptions['socialProviders']","plugins: Array<BetterAuthPlugin>","oktaConfig: Array<ReturnType<typeof okta>>","t","upload","outBuffer: Uint8Array","buf: Buffer","content: Uint8Array | Buffer","width: number | null","height: number | null","FEATURES: Array<FeatureRegistration>","router","dbManager: EhDatabaseManager | null","router","middlewareContext: MiddlewareContext"],"sources":["../src/db/client.ts","../src/modules/appCatalog/service.ts","../src/db/tableSyncPrismaAdapter.ts","../src/db/tableSyncMagazine.ts","../src/db/syncAppCatalog.ts","../src/modules/appCatalogAdmin/appCatalogAdminRouter.ts","../src/modules/assets/screenshotRouter.ts","../src/modules/auth/authRouter.ts","../src/modules/assets/assetUtils.ts","../src/modules/icons/iconRouter.ts","../src/server/controller.ts","../src/server/ehTrpcContext.ts","../src/server/ehStaticControllerContract.ts","../src/modules/auth/auth.ts","../src/modules/auth/registerAuthRoutes.ts","../src/modules/auth/authProviders.ts","../src/modules/auth/authorizationUtils.ts","../src/modules/admin/chat/createAdminChatHandler.ts","../src/modules/admin/chat/createDatabaseTools.ts","../src/modules/icons/iconRestController.ts","../src/modules/icons/iconService.ts","../src/modules/assets/assetRestController.ts","../src/modules/assets/screenshotRestController.ts","../src/modules/assets/syncAssets.ts","../src/middleware/database.ts","../src/middleware/backendResolver.ts","../src/middleware/featureRegistry.ts","../src/middleware/createEhMiddleware.ts"],"sourcesContent":["import { PrismaClient } from '@prisma/client'\n\nlet prismaClient: PrismaClient | null = null\n\n/**\n * Gets the internal Prisma client instance.\n * Creates one if it doesn't exist.\n */\nexport function getDbClient(): PrismaClient {\n if (!prismaClient) {\n prismaClient = new PrismaClient()\n }\n return prismaClient\n}\n\n/**\n * Sets the internal Prisma client instance.\n * Used by middleware to bridge with existing getDbClient() usage.\n */\nexport function setDbClient(client: PrismaClient): void {\n prismaClient = client\n}\n\n/**\n * Connects to the database.\n * Call this before performing database operations.\n */\nexport async function connectDb(): Promise<void> {\n const client = getDbClient()\n await client.$connect()\n}\n\n/**\n * Disconnects from the database.\n * Call this when done with database operations (e.g., in scripts).\n */\nexport async function disconnectDb(): Promise<void> {\n if (prismaClient) {\n await prismaClient.$disconnect()\n prismaClient = null\n }\n}\n","import { getDbClient } from '../../db/client'\nimport type {\n AppCatalogData,\n AppCategory,\n AppForCatalog,\n} from '../../types/common/appCatalogTypes'\n\nfunction capitalize(word: string): string {\n if (!word) return word\n return word.charAt(0).toUpperCase() + word.slice(1)\n}\n\nexport async function getAppsFromPrisma(): Promise<Array<AppForCatalog>> {\n const prisma = getDbClient()\n\n // Fetch all apps\n const rows = await prisma.dbAppForCatalog.findMany()\n\n return rows.map((row) => {\n const access = row.access as unknown as AppForCatalog['access']\n const roles =\n row.roles == null\n ? undefined\n : (row.roles as unknown as AppForCatalog['roles'])\n const teams = (row.teams as unknown as Array<string> | null) ?? []\n const tags = (row.tags as unknown as AppForCatalog['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as AppForCatalog['screenshotIds']) ?? []\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n\n return {\n id: row.id,\n slug: row.slug,\n displayName: row.displayName,\n description: row.description,\n access,\n teams,\n roles,\n approver:\n row.approverName && row.approverEmail\n ? { name: row.approverName, email: row.approverEmail }\n : undefined,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n }\n })\n}\n\nexport function deriveCategories(\n apps: Array<AppForCatalog>,\n): Array<AppCategory> {\n const tagSet = new Set<string>()\n for (const app of apps) {\n for (const tag of app.tags ?? []) {\n const normalized = tag.trim().toLowerCase()\n if (normalized) tagSet.add(normalized)\n }\n }\n const categories: Array<AppCategory> = [{ id: 'all', name: 'All' }]\n for (const tag of Array.from(tagSet).sort()) {\n categories.push({ id: tag, name: capitalize(tag) })\n }\n return categories\n}\n\nexport async function getAppCatalogData(\n getAppsOptional?: () => Promise<Array<AppForCatalog>>,\n): Promise<AppCatalogData> {\n const apps = getAppsOptional\n ? await getAppsOptional()\n : await getAppsFromPrisma()\n const categories = deriveCategories(apps)\n return { apps, categories }\n}\n","import { tableSync } from '@env-hopper/table-sync'\nimport type { Prisma, PrismaClient } from '@prisma/client'\nimport type * as runtime from '@prisma/client/runtime/library'\nimport { mapValues, omit, pick } from 'radashi'\n\nexport type ScalarKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\nexport type ObjectKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['objects']\n\nexport type ScalarFilter<TPrismaModelName extends Prisma.ModelName> = Partial<\n Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\n>\n\nexport type GetOperationFns<TModel extends Prisma.ModelName> = {\n [TOperation in keyof Prisma.TypeMap['model']['DbAppForCatalog']['operations']]: (\n args: Prisma.TypeMap['model'][TModel]['operations'][TOperation]['args'],\n ) => Promise<\n Prisma.TypeMap['model'][TModel]['operations'][TOperation]['result']\n >\n}\n\nexport interface TableSyncParamsPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,\n TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,\n> {\n prisma: TPrismaClient\n prismaModelName: TPrismaModelName\n uniqColumns: TUniqColumns\n relationColumns?: TRelationColumns\n where?: ScalarFilter<TPrismaModelName>\n upsertOnly?: boolean\n}\n\nfunction getPrismaModelOperations<\n TPrismaClient extends Omit<PrismaClient, runtime.ITXClientDenyList>,\n TPrismaModelName extends Prisma.ModelName,\n>(prisma: TPrismaClient, prismaModelName: TPrismaModelName) {\n const key = (prismaModelName.slice(0, 1).toLowerCase() +\n prismaModelName.slice(1)) as keyof TPrismaClient\n return prisma[key] as GetOperationFns<TPrismaModelName>\n}\n\nexport type MakeTFromPrismaModel<TPrismaModelName extends Prisma.ModelName> =\n NonNullable<\n Prisma.TypeMap['model'][TPrismaModelName]['operations']['findUnique']['result']\n >\n\nexport function tableSyncPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,\n TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,\n TId extends ScalarKeys<TPrismaModelName> & (string | number) = 'id',\n>(\n params: TableSyncParamsPrisma<\n TPrismaClient,\n TPrismaModelName,\n TUniqColumns,\n TRelationColumns\n >,\n) {\n const {\n prisma,\n prismaModelName,\n uniqColumns,\n where: whereGlobal,\n upsertOnly,\n } = params\n const prismOperations = getPrismaModelOperations(prisma, prismaModelName)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n return tableSync<MakeTFromPrismaModel<TPrismaModelName>, TUniqColumns, TId>({\n id: 'id' as TId,\n uniqColumns,\n readAll: async () => {\n const findManyArgs = whereGlobal\n ? {\n where: whereGlobal,\n }\n : {}\n return (await prismOperations.findMany(findManyArgs)) as Array<\n MakeTFromPrismaModel<TPrismaModelName>\n >\n },\n writeAll: async (createData, update, deleteIds) => {\n const prismaUniqKey = params.uniqColumns.join('_')\n const relationColumnList =\n params.relationColumns ?? ([] as Array<ObjectKeys<TPrismaModelName>>)\n\n return prisma.$transaction(async (tx) => {\n const txOps = getPrismaModelOperations(tx, prismaModelName)\n for (const { data, where } of update) {\n const uniqKeyWhere =\n Object.keys(where).length > 1\n ? {\n [prismaUniqKey]: where,\n }\n : where\n\n const dataScalar = omit(data, relationColumnList)\n const dataRelations = mapValues(\n pick(data, relationColumnList),\n (value) => {\n return {\n set: value,\n }\n },\n )\n\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.update({\n data: { ...dataScalar, ...dataRelations },\n where: { ...uniqKeyWhere },\n })\n }\n\n if (upsertOnly !== true) {\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.deleteMany({\n where: {\n id: {\n in: deleteIds,\n },\n },\n })\n }\n\n // Validate uniqueness of uniqColumns before creating records\n const createDataMapped = createData.map((data) => {\n // @ts-expect-error This is too difficult for me to come up with right types\n const dataScalar = omit(data, relationColumnList)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n const onlyRelationColumns = pick(data, relationColumnList)\n const dataRelations = mapValues(onlyRelationColumns, (value) => {\n return {\n connect: value,\n }\n })\n\n return { ...dataScalar, ...dataRelations }\n })\n\n // Check for duplicates in the data to be created\n if (createDataMapped.length > 0) {\n const uniqKeysInCreate = new Set<string>()\n const duplicateKeys: Array<string> = []\n\n for (const data of createDataMapped) {\n const keyParts = params.uniqColumns.map((col) => {\n const value = data[col as keyof typeof data]\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive: unique columns may be nullable in schemas\n return value === null || value === undefined\n ? 'null'\n : String(value)\n })\n const key = keyParts.join(':')\n\n if (uniqKeysInCreate.has(key)) {\n duplicateKeys.push(key)\n } else {\n uniqKeysInCreate.add(key)\n }\n }\n\n if (duplicateKeys.length > 0) {\n const uniqColumnsStr = params.uniqColumns.join(', ')\n throw new Error(\n `Duplicate unique key values found in data to be created. ` +\n `Model: ${prismaModelName}, Unique columns: [${uniqColumnsStr}], ` +\n `Duplicate keys: [${duplicateKeys.join(', ')}]`,\n )\n }\n }\n\n const results: Array<MakeTFromPrismaModel<TPrismaModelName>> = []\n\n if (relationColumnList.length === 0) {\n // @ts-expect-error This is too difficult for me to come up with right types\n const batchResult = await txOps.createManyAndReturn({\n data: createDataMapped,\n })\n\n results.push(...batchResult)\n } else {\n for (const dataMappedElement of createDataMapped) {\n // @ts-expect-error too difficult for me\n const newVar = await txOps.create({\n data: dataMappedElement,\n })\n results.push(newVar as MakeTFromPrismaModel<TPrismaModelName>)\n }\n }\n\n return results\n })\n },\n })\n}\n","import type { Prisma } from '@prisma/client'\nimport type { ObjectKeys, ScalarKeys } from './tableSyncPrismaAdapter'\n\ninterface CommonSyncTableInfo<TPrismaModelName extends Prisma.ModelName> {\n prismaModelName: TPrismaModelName\n uniqColumns: Array<ScalarKeys<TPrismaModelName>>\n relationColumns?: Array<ObjectKeys<TPrismaModelName>>\n}\n\ntype TableSyncMagazineType = Partial<{\n [key in Prisma.ModelName]: CommonSyncTableInfo<key>\n}>\n\nexport const TABLE_SYNC_MAGAZINE = {\n DbAppForCatalog: {\n prismaModelName: 'DbAppForCatalog',\n uniqColumns: ['slug'],\n },\n} as const satisfies TableSyncMagazineType\n\nexport type TableSyncMagazine = typeof TABLE_SYNC_MAGAZINE\nexport type TableSyncMagazineModelNameKey = keyof TableSyncMagazine\n","import type { AppForCatalog } from '../types/common/appCatalogTypes'\nimport { getDbClient } from './client'\nimport { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'\nimport { tableSyncPrisma } from './tableSyncPrismaAdapter'\n\nexport interface SyncAppCatalogResult {\n created: number\n updated: number\n deleted: number\n total: number\n}\n\n/**\n * Syncs app catalog data to the database using table sync.\n * This will create new apps, update existing ones, and delete any that are no longer in the input.\n *\n * Note: Call connectDb() before and disconnectDb() after if running in a script.\n */\nexport async function syncAppCatalog(\n apps: Array<AppForCatalog>,\n): Promise<SyncAppCatalogResult> {\n const prisma = getDbClient()\n\n const sync = tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbAppForCatalog,\n })\n\n // Transform AppForCatalog to DbAppForCatalog format\n const dbApps = apps.map((app) => {\n const slug =\n app.slug ||\n app.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return {\n slug,\n displayName: app.displayName,\n description: app.description,\n access: app.access,\n teams: app.teams ?? [],\n roles: app.roles ?? null,\n approverName: app.approver?.name ?? null,\n approverEmail: app.approver?.email ?? null,\n notes: app.notes ?? null,\n tags: app.tags ?? [],\n appUrl: app.appUrl ?? null,\n links: app.links ?? null,\n iconName: app.iconName ?? null,\n screenshotIds: app.screenshotIds ?? [],\n }\n })\n\n const result = await sync.sync(dbApps)\n\n // Get actual synced data to calculate stats\n const actual = result.getActual()\n\n return {\n created: actual.length - apps.length + (apps.length - actual.length),\n updated: 0, // TableSync doesn't expose this directly\n deleted: 0, // TableSync doesn't expose this directly\n total: actual.length,\n }\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\n\n// Zod schema for access method (simplified for now - you can expand this)\nconst AccessMethodSchema = z.object({\n type: z.enum(['bot', 'ticketing', 'email', 'self-service', 'documentation', 'manual']),\n}).passthrough()\n\nconst AppLinkSchema = z.object({\n displayName: z.string().optional(),\n url: z.string().url(),\n})\n\nconst AppRoleSchema = z.object({\n id: z.string(),\n name: z.string(),\n description: z.string().optional(),\n})\n\nconst ApproverSchema = z.object({\n name: z.string(),\n email: z.string().email(),\n})\n\nconst CreateAppForCatalogSchema = z.object({\n slug: z.string().min(1).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),\n displayName: z.string().min(1),\n description: z.string(),\n access: AccessMethodSchema,\n teams: z.array(z.string()).optional(),\n roles: z.array(AppRoleSchema).optional(),\n approver: ApproverSchema.optional(),\n notes: z.string().optional(),\n tags: z.array(z.string()).optional(),\n appUrl: z.string().url().optional(),\n links: z.array(AppLinkSchema).optional(),\n iconName: z.string().optional(),\n screenshotIds: z.array(z.string()).optional(),\n})\n\nconst UpdateAppForCatalogSchema = CreateAppForCatalogSchema.partial().extend({\n id: z.string(),\n})\n\nexport function createAppCatalogAdminRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.findMany({\n orderBy: { displayName: 'asc' },\n })\n }),\n\n getById: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.findUnique({\n where: { id: input.id },\n })\n }),\n\n create: publicProcedure\n .input(CreateAppForCatalogSchema)\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { approver, ...rest } = input\n\n return prisma.dbAppForCatalog.create({\n data: {\n ...rest,\n approverName: approver?.name ?? null,\n approverEmail: approver?.email ?? null,\n teams: input.teams ?? [],\n tags: input.tags ?? [],\n screenshotIds: input.screenshotIds ?? [],\n },\n })\n }),\n\n update: publicProcedure\n .input(UpdateAppForCatalogSchema)\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { id, approver, ...rest } = input\n\n return prisma.dbAppForCatalog.update({\n where: { id },\n data: {\n ...rest,\n ...(approver !== undefined && {\n approverName: approver.name || null,\n approverEmail: approver.email || null,\n }),\n },\n })\n }),\n\n delete: publicProcedure\n .input(z.object({ id: z.string() }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAppForCatalog.delete({\n where: { id: input.id },\n })\n }),\n })\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\n\nexport function createScreenshotRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAsset.findMany({\n where: { assetType: 'screenshot' },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n orderBy: { createdAt: 'desc' },\n })\n }),\n\n getOne: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n\n getByAppSlug: publicProcedure\n .input(z.object({ appSlug: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: input.appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app) {\n return []\n }\n\n // Fetch all screenshots for the app\n return prisma.dbAsset.findMany({\n where: {\n id: { in: app.screenshotIds },\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n\n getFirstByAppSlug: publicProcedure\n .input(z.object({ appSlug: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: input.appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app || app.screenshotIds.length === 0) {\n return null\n }\n\n // Fetch first screenshot\n return prisma.dbAsset.findUnique({\n where: { id: app.screenshotIds[0] },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n }),\n })\n}\n","import type { BetterAuthPlugin } from 'better-auth'\nimport type { TRPCRootObject } from '@trpc/server'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\nimport type { BetterAuth } from './auth'\n\n/**\n * Create auth tRPC procedures\n * @param t - tRPC instance\n * @param auth - Better Auth instance (optional, for future extensions)\n * @returns tRPC router with auth procedures\n */\nexport function createAuthRouter(\n t: TRPCRootObject<EhTrpcContext, {}, {}>,\n auth?: BetterAuth,\n) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n getSession: publicProcedure.query(async ({ ctx }) => {\n // Session will be extracted from cookies by better-auth middleware\n // For now, return user info if available in context\n const contextWithUser = ctx as EhTrpcContext & { user?: unknown }\n return {\n user: contextWithUser.user ?? null,\n isAuthenticated: !!contextWithUser.user,\n }\n }),\n getProviders: publicProcedure.query(() => {\n // Return configured social providers and OAuth providers from plugins\n const providers: Array<string> = []\n const authOptions = auth?.options\n\n // Add built-in social providers (github, google, etc.)\n if (authOptions?.socialProviders) {\n const socialProviders = authOptions.socialProviders as Record<\n string,\n unknown\n >\n Object.keys(socialProviders).forEach((key) => {\n if (socialProviders[key]) {\n providers.push(key)\n }\n })\n }\n\n // Add OAuth providers from plugins (like Okta via genericOAuth)\n if (authOptions?.plugins) {\n const plugins = authOptions.plugins\n plugins.forEach((plugin) => {\n const pluginWithConfig = plugin as BetterAuthPlugin & {\n options?: {\n config?: Array<{ providerId?: string }>\n }\n }\n if (\n pluginWithConfig.id === 'generic-oauth' &&\n pluginWithConfig.options?.config\n ) {\n const configs = Array.isArray(pluginWithConfig.options.config)\n ? pluginWithConfig.options.config\n : [pluginWithConfig.options.config]\n configs.forEach((config) => {\n if (config.providerId) {\n providers.push(config.providerId)\n }\n })\n }\n })\n }\n\n return { providers }\n }),\n })\n}\n\nexport type AuthRouter = ReturnType<typeof createAuthRouter>\n","import { createHash } from 'node:crypto';\nimport sharp from 'sharp';\n\n/**\n * Extract image dimensions from a buffer using sharp\n */\nexport async function getImageDimensions(\n buffer: Buffer,\n): Promise<{ width: number | null; height: number | null }> {\n try {\n const metadata = await sharp(buffer).metadata()\n return {\n width: metadata.width ?? null,\n height: metadata.height ?? null,\n }\n } catch (error) {\n console.error('Error extracting image dimensions:', error)\n return { width: null, height: null }\n }\n}\n\n/**\n * Resize an image buffer to the specified dimensions\n * @param buffer - The image buffer to resize\n * @param width - Target width (optional)\n * @param height - Target height (optional)\n * @param format - Output format ('png', 'jpeg', 'webp'), auto-detected if not provided\n */\nexport async function resizeImage(\n buffer: Buffer,\n width?: number,\n height?: number,\n format?: 'png' | 'jpeg' | 'webp',\n): Promise<Buffer> {\n let pipeline = sharp(buffer)\n\n // Apply resize if dimensions provided\n if (width || height) {\n pipeline = pipeline.resize({\n width,\n height,\n fit: 'inside',\n withoutEnlargement: true,\n })\n }\n\n // Apply format conversion if specified\n if (format === 'png') {\n pipeline = pipeline.png()\n } else if (format === 'webp') {\n pipeline = pipeline.webp()\n } else if (format === 'jpeg') {\n pipeline = pipeline.jpeg()\n }\n\n return pipeline.toBuffer()\n}\n\n/**\n * Generate SHA-256 checksum for a buffer\n */\nexport function generateChecksum(buffer: Buffer): string {\n return createHash('sha256').update(buffer).digest('hex')\n}\n\n/**\n * Detect image format from mime type\n */\nexport function getImageFormat(mimeType: string): 'png' | 'webp' | 'jpeg' | null {\n if (mimeType.includes('png')) return 'png'\n if (mimeType.includes('webp')) return 'webp'\n if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'jpeg'\n return null\n}\n\n/**\n * Check if a mime type represents a raster image (not SVG)\n */\nexport function isRasterImage(mimeType: string): boolean {\n return mimeType.startsWith('image/') && !mimeType.includes('svg')\n}\n","import type { TRPCRootObject } from '@trpc/server'\nimport { z } from 'zod'\nimport { getDbClient } from '../../db'\nimport type { EhTrpcContext } from '../../server/ehTrpcContext'\nimport { generateChecksum, getImageDimensions } from '../assets/assetUtils'\n\nexport function createIconRouter(t: TRPCRootObject<EhTrpcContext, {}, {}>) {\n const router = t.router\n const publicProcedure = t.procedure\n\n return router({\n list: publicProcedure.query(async () => {\n const prisma = getDbClient()\n return prisma.dbAsset.findMany({\n where: { assetType: 'icon' },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n createdAt: true,\n updatedAt: true,\n },\n orderBy: { name: 'asc' },\n })\n }),\n\n getOne: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'icon',\n },\n })\n }),\n\n create: publicProcedure\n .input(\n z.object({\n name: z.string().min(1),\n content: z.string(), // base64 encoded binary\n mimeType: z.string(),\n fileSize: z.number().int().positive(),\n }),\n )\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n // Convert base64 to Buffer\n const buffer = Buffer.from(input.content, 'base64')\n \n // Generate checksum and extract dimensions\n const checksum = generateChecksum(buffer)\n const { width, height } = await getImageDimensions(buffer)\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n // Return existing asset if content is identical\n return existing\n }\n\n return prisma.dbAsset.create({\n data: {\n name: input.name,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n })\n }),\n\n update: publicProcedure\n .input(\n z.object({\n id: z.string(),\n name: z.string().min(1).optional(),\n content: z.string().optional(), // base64 encoded binary\n mimeType: z.string().optional(),\n fileSize: z.number().int().positive().optional(),\n }),\n )\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n const { id, content, ...rest } = input\n\n const data: Record<string, unknown> = { ...rest }\n \n if (content) {\n const buffer = Buffer.from(content, 'base64')\n data.content = new Uint8Array(buffer)\n data.checksum = generateChecksum(buffer)\n \n const { width, height } = await getImageDimensions(buffer)\n data.width = width\n data.height = height\n }\n\n return prisma.dbAsset.update({\n where: { id },\n data,\n })\n }),\n\n delete: publicProcedure\n .input(z.object({ id: z.string() }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.delete({\n where: { id: input.id },\n })\n }),\n\n deleteMany: publicProcedure\n .input(z.object({ ids: z.array(z.string()) }))\n .mutation(async ({ input }) => {\n const prisma = getDbClient()\n return prisma.dbAsset.deleteMany({\n where: {\n id: { in: input.ids },\n assetType: 'icon',\n },\n })\n }),\n\n // Serve icon binary content\n getContent: publicProcedure\n .input(z.object({ id: z.string() }))\n .query(async ({ input }) => {\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findFirst({\n where: {\n id: input.id,\n assetType: 'icon',\n },\n select: { content: true, mimeType: true },\n })\n if (!asset) {\n throw new Error('Icon not found')\n }\n // Return base64 encoded content\n return {\n content: Buffer.from(asset.content).toString('base64'),\n mimeType: asset.mimeType,\n }\n }),\n })\n}\n","import { initTRPC } from '@trpc/server'\nimport z from 'zod'\n\nimport { getAppCatalogData } from '../modules/appCatalog/service'\nimport type { AppCatalogData, BootstrapConfigData, ResourceJumpsData } from '../types'\n\nimport type { TRPCRootObject } from '@trpc/server'\n\nimport { createAppCatalogAdminRouter } from '../modules/appCatalogAdmin/appCatalogAdminRouter.js'\nimport { createScreenshotRouter } from '../modules/assets/screenshotRouter.js'\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createIconRouter } from '../modules/icons/iconRouter.js'\nimport type { EhTrpcContext } from './ehTrpcContext'\n\n/**\n * Initialization of tRPC backend\n * Should be done only once per backend!\n */\nconst t: TRPCRootObject<EhTrpcContext, {}, {}> = initTRPC\n .context<EhTrpcContext>()\n .create({\n errorFormatter({ error, shape }: { error: unknown; shape: unknown }) {\n // Log all tRPC errors to console\n console.error('[tRPC Error]', {\n path: (shape as { data?: { path?: string } }).data?.path,\n code: (error as { code?: string }).code,\n message: (error as { message?: string }).message,\n cause: (error as { cause?: unknown }).cause,\n stack: (error as { stack?: string }).stack,\n })\n return shape\n },\n })\n\n/**\n * Export reusable router and procedure helpers\n * that can be used throughout the router\n */\nconst router: typeof t.router = t.router\nconst publicProcedure: typeof t.procedure = t.procedure\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(auth?: BetterAuth) {\n return router({\n bootstrap: publicProcedure.query(\n async ({ ctx }): Promise<BootstrapConfigData> => {\n return await ctx.companySpecificBackend.getBootstrapData()\n },\n ),\n\n availabilityMatrix: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getAvailabilityMatrix()\n }),\n\n tryFindRenameRule: publicProcedure\n .input(\n z.object({\n envSlug: z.string().optional(),\n resourceSlug: z.string().optional(),\n }),\n )\n .query(async ({ input, ctx }) => {\n return await ctx.companySpecificBackend.getNameMigrations(input)\n }),\n\n resourceJumps: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getResourceJumps()\n }),\n\n resourceJumpsExtended: publicProcedure.query(async ({ ctx }) => {\n return await ctx.companySpecificBackend.getResourceJumpsExtended()\n }),\n resourceJumpBySlugAndEnv: publicProcedure\n .input(\n z.object({\n jumpResourceSlug: z.string(),\n envSlug: z.string(),\n }),\n )\n .query(async ({ input, ctx }) => {\n return filterSingleResourceJump(\n await ctx.companySpecificBackend.getResourceJumps(),\n input.jumpResourceSlug,\n input.envSlug,\n )\n }),\n\n appCatalog: publicProcedure.query(async ({ ctx }): Promise<AppCatalogData> => {\n return await getAppCatalogData(ctx.companySpecificBackend.getApps)\n }),\n\n // Icon management routes\n icon: createIconRouter(t),\n\n // Screenshot management routes\n screenshot: createScreenshotRouter(t),\n\n // App catalog admin routes\n appCatalogAdmin: createAppCatalogAdminRouter(t),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth),\n })\n}\n\nfunction filterSingleResourceJump(\n resourceJumps: ResourceJumpsData,\n jumpResourceSlug: string,\n envSlug: string,\n): ResourceJumpsData {\n const filteredResourceJump = resourceJumps.resourceJumps.find(\n (item) => item.slug === jumpResourceSlug,\n )\n const filteredEnv = resourceJumps.envs.find((item) => item.slug === envSlug)\n\n return {\n resourceJumps: filteredResourceJump ? [filteredResourceJump] : [],\n envs: filteredEnv ? [filteredEnv] : [],\n lateResolvableParams: resourceJumps.lateResolvableParams,\n }\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n","import type { EhBackendCompanySpecificBackend } from '../types'\n\nexport interface EhTrpcContext {\n companySpecificBackend: EhBackendCompanySpecificBackend\n}\n\nexport interface EhTrpcContextOptions {\n companySpecificBackend: EhBackendCompanySpecificBackend\n}\n\nexport function createEhTrpcContext({\n companySpecificBackend,\n}: EhTrpcContextOptions): EhTrpcContext {\n return {\n companySpecificBackend,\n }\n}\n","export interface EhStaticControllerContract {\n methods: { \n getIcon: { method: string; url: string }\n getScreenshot: { method: string; url: string }\n }\n}\n\nexport const staticControllerContract: EhStaticControllerContract = {\n methods: {\n getIcon: {\n method: 'get',\n url: 'icon/:icon',\n },\n getScreenshot: {\n method: 'get',\n url: 'screenshot/:id',\n },\n },\n}\n","import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'\nimport { betterAuth } from 'better-auth'\nimport { prismaAdapter } from 'better-auth/adapters/prisma'\nimport { getDbClient } from '../../db'\n\nexport interface AuthConfig {\n appName?: string\n baseURL: string\n secret: string\n providers?: BetterAuthOptions['socialProviders']\n plugins?: Array<BetterAuthPlugin>\n /** Session expiration in seconds. Default: 7 days (604800) */\n sessionExpiresIn?: number\n /** Session update age in seconds. Default: 1 day (86400) */\n sessionUpdateAge?: number\n}\n\nexport function createAuth(config: AuthConfig) {\n const prisma = getDbClient()\n const isProduction = process.env.NODE_ENV === 'production'\n\n const auth = betterAuth({\n appName: config.appName || 'EnvHopper',\n baseURL: config.baseURL,\n basePath: '/api/auth',\n secret: config.secret,\n database: prismaAdapter(prisma, {\n provider: 'postgresql',\n }),\n socialProviders: config.providers || {},\n plugins: config.plugins || [],\n emailAndPassword: {\n enabled: true,\n },\n session: {\n expiresIn: config.sessionExpiresIn ?? 60 * 60 * 24 * 30,\n updateAge: config.sessionUpdateAge ?? 60 * 60 * 24,\n cookieCache: {\n enabled: true,\n maxAge: 300,\n },\n },\n advanced: {\n useSecureCookies: isProduction,\n },\n })\n\n return auth\n}\n\nexport type BetterAuth = ReturnType<typeof createAuth>\n","import { toNodeHandler } from 'better-auth/node'\nimport type { Express, Request, Response } from 'express'\nimport type { BetterAuth } from './auth'\n\n/**\n * Register Better Auth routes with Express\n * @param app - Express application instance\n * @param auth - Better Auth instance\n */\nexport function registerAuthRoutes(app: Express, auth: BetterAuth) {\n // Explicit session endpoint handler\n // Better Auth's toNodeHandler doesn't expose a direct /session endpoint\n app.get('/api/auth/session', async (req: Request, res: Response) => {\n try {\n const session = await auth.api.getSession({\n headers: req.headers as HeadersInit,\n })\n if (session) {\n res.json(session)\n } else {\n res.status(401).json({ error: 'Not authenticated' })\n }\n } catch (error) {\n console.error('[Auth Session Error]', error)\n res.status(500).json({ error: 'Internal server error' })\n }\n })\n\n // Use toNodeHandler to adapt better-auth for Express/Node.js\n // Express v5 wildcard syntax: /{*any} (also works with Express v4)\n const authHandler = toNodeHandler(auth)\n app.all('/api/auth/{*any}', authHandler)\n}\n","/**\n * Auth provider configuration from environment variables\n * This is the recommended way to configure auth providers.\n *\n * Supports: GitHub, Google via environment variables\n * For Okta and other custom providers, use getAuthPluginsFromEnv()\n *\n * Example .env:\n * AUTH_GITHUB_CLIENT_ID=your_github_client_id\n * AUTH_GITHUB_CLIENT_SECRET=your_github_client_secret\n * AUTH_GOOGLE_CLIENT_ID=your_google_client_id\n * AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret\n */\n\nimport type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'\nimport { genericOAuth, okta } from 'better-auth/plugins'\n\nexport function getAuthProvidersFromEnv(): BetterAuthOptions['socialProviders'] {\n const providers: BetterAuthOptions['socialProviders'] = {}\n\n // GitHub OAuth\n if (\n process.env.AUTH_GITHUB_CLIENT_ID &&\n process.env.AUTH_GITHUB_CLIENT_SECRET\n ) {\n providers.github = {\n clientId: process.env.AUTH_GITHUB_CLIENT_ID,\n clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET,\n }\n }\n\n // Google OAuth\n if (\n process.env.AUTH_GOOGLE_CLIENT_ID &&\n process.env.AUTH_GOOGLE_CLIENT_SECRET\n ) {\n providers.google = {\n clientId: process.env.AUTH_GOOGLE_CLIENT_ID,\n clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,\n }\n }\n\n return providers\n}\n\n/**\n * Get auth plugins from environment variables\n * Currently supports: Okta\n *\n * Example .env:\n * AUTH_OKTA_CLIENT_ID=your_okta_client_id\n * AUTH_OKTA_CLIENT_SECRET=your_okta_client_secret\n * AUTH_OKTA_ISSUER=https://your-org.okta.com/oauth2/ausxb83g4wY1x09ec0h7\n *\n * Note: If you get \"User is not assigned to the client application\" errors,\n * you need to configure your Okta application to allow all users:\n * 1. In Okta Admin Console, go to Applications → Your App\n * 2. Assignments tab → Assign to Groups → Add \"Everyone\" group\n * OR\n * 3. Edit the application → In \"User consent\" section, enable appropriate settings\n *\n * For group-based authorization:\n * 1. Add \"groups\" scope to your auth server policy rule\n * 2. Create a groups claim in your auth server\n * 3. Groups will be available in the user object after authentication\n */\nexport function getAuthPluginsFromEnv(): Array<BetterAuthPlugin> {\n const plugins: Array<BetterAuthPlugin> = []\n const oktaConfig: Array<ReturnType<typeof okta>> = []\n\n if (\n process.env.AUTH_OKTA_CLIENT_ID &&\n process.env.AUTH_OKTA_CLIENT_SECRET &&\n process.env.AUTH_OKTA_ISSUER\n ) {\n oktaConfig.push(\n okta({\n clientId: process.env.AUTH_OKTA_CLIENT_ID,\n clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET,\n issuer: process.env.AUTH_OKTA_ISSUER,\n }),\n )\n }\n\n if (oktaConfig.length > 0) {\n plugins.push(genericOAuth({ config: oktaConfig }))\n }\n\n return plugins\n}\n\n/**\n * Validate required auth environment variables\n */\nexport function validateAuthConfig(): void {\n const secret = process.env.BETTER_AUTH_SECRET\n const baseUrl = process.env.BETTER_AUTH_URL\n\n if (!secret) {\n console.warn(\n 'BETTER_AUTH_SECRET not set. Using development fallback. Set this in production!',\n )\n }\n\n if (!baseUrl) {\n console.info('BETTER_AUTH_URL not set. Using default http://localhost:3000')\n }\n}\n","/**\n * Authorization utilities for checking user permissions based on groups\n *\n * Groups are automatically included in the user session when:\n * 1. Okta auth server has a \"groups\" claim configured\n * 2. The auth policy rule includes \"groups\" in scope_whitelist\n *\n * Example usage in tRPC procedures:\n * ```typescript\n * myProcedure: protectedProcedure.query(async ({ ctx }) => {\n * if (requireAdmin(ctx.user)) {\n * // Admin-only logic\n * }\n * // Regular user logic\n * })\n * ```\n */\n\nexport interface UserWithGroups {\n id: string\n email: string\n name?: string\n // Groups from Okta (or other identity provider)\n // This will be populated if groups claim is configured\n [key: string]: any\n}\n\n/**\n * Extract groups from user object\n * Groups can be stored in different locations depending on the OAuth provider\n */\nexport function getUserGroups(\n user: UserWithGroups | null | undefined,\n): Array<string> {\n if (!user) {\n return []\n }\n\n // Check common locations for group information\n const groups =\n user.groups || // Standard \"groups\" claim\n (user as any).env_hopper_groups || // Custom env_hopper_groups claim\n (user as any).oktaGroups || // Okta-specific\n (user as any).roles || // Some providers use \"roles\"\n []\n\n return Array.isArray(groups) ? groups : []\n}\n\n/**\n * Check if user is a member of any of the specified groups\n */\nexport function isMemberOfAnyGroup(\n user: UserWithGroups | null | undefined,\n allowedGroups: Array<string>,\n): boolean {\n const userGroups = getUserGroups(user)\n return allowedGroups.some((group) => userGroups.includes(group))\n}\n\n/**\n * Check if user is a member of all specified groups\n */\nexport function isMemberOfAllGroups(\n user: UserWithGroups | null | undefined,\n requiredGroups: Array<string>,\n): boolean {\n const userGroups = getUserGroups(user)\n return requiredGroups.every((group) => userGroups.includes(group))\n}\n\n/**\n * Get admin group names from environment variables\n * Default: env_hopper_ui_super_admins\n */\nexport function getAdminGroupsFromEnv(): Array<string> {\n const adminGroups =\n process.env.AUTH_ADMIN_GROUPS || 'env_hopper_ui_super_admins'\n return adminGroups\n .split(',')\n .map((g) => g.trim())\n .filter(Boolean)\n}\n\n/**\n * Check if user has admin permissions\n */\nexport function isAdmin(user: UserWithGroups | null | undefined): boolean {\n const adminGroups = getAdminGroupsFromEnv()\n return isMemberOfAnyGroup(user, adminGroups)\n}\n\n/**\n * Require admin permissions - throws error if not admin\n */\nexport function requireAdmin(user: UserWithGroups | null | undefined): void {\n if (!isAdmin(user)) {\n throw new Error('Forbidden: Admin access required')\n }\n}\n\n/**\n * Require membership in specific groups - throws error if not member\n */\nexport function requireGroups(\n user: UserWithGroups | null | undefined,\n groups: Array<string>,\n): void {\n if (!isMemberOfAnyGroup(user, groups)) {\n throw new Error(\n `Forbidden: Membership in one of these groups required: ${groups.join(', ')}`,\n )\n }\n}\n","import { stepCountIs, streamText, tool } from 'ai'\nimport type { LanguageModel, Tool } from 'ai'\n\nimport type { Request, Response } from 'express'\n\nexport interface AdminChatHandlerOptions {\n /** The AI model to use (from @ai-sdk/openai, @ai-sdk/anthropic, etc.) */\n model: LanguageModel\n /** System prompt for the AI assistant */\n systemPrompt?: string\n /** Tools available to the AI assistant */\n tools?: Record<string, Tool>\n /**\n * Optional function to validate configuration before processing requests.\n * Should throw an error if configuration is invalid (e.g., missing API key).\n * @example\n * validateConfig: () => {\n * if (!process.env.OPENAI_API_KEY) {\n * throw new Error('OPENAI_API_KEY is not configured')\n * }\n * }\n */\n validateConfig?: () => void\n}\n\ninterface TextPart {\n type: 'text'\n text: string\n}\n\ninterface UIMessageInput {\n role: 'user' | 'assistant' | 'system'\n content?: string\n parts?: Array<TextPart | { type: string }>\n}\n\ninterface CoreMessage {\n role: 'user' | 'assistant' | 'system'\n content: string\n}\n\nfunction convertToCoreMessages(\n messages: Array<UIMessageInput>,\n): Array<CoreMessage> {\n return messages.map((msg) => {\n if (msg.content) {\n return { role: msg.role, content: msg.content }\n }\n // Extract text from parts array (AI SDK v3 format)\n const textContent =\n msg.parts\n ?.filter((part): part is TextPart => part.type === 'text')\n .map((part) => part.text)\n .join('') ?? ''\n return { role: msg.role, content: textContent }\n })\n}\n\n/**\n * Creates an Express handler for the admin chat endpoint.\n *\n * Usage in thin wrappers:\n *\n * ```typescript\n * // With OpenAI\n * import { openai } from '@ai-sdk/openai'\n * app.post('/api/admin/chat', createAdminChatHandler({\n * model: openai('gpt-4o-mini'),\n * }))\n *\n * // With Claude\n * import { anthropic } from '@ai-sdk/anthropic'\n * app.post('/api/admin/chat', createAdminChatHandler({\n * model: anthropic('claude-sonnet-4-20250514'),\n * }))\n * ```\n */\nexport function createAdminChatHandler(options: AdminChatHandlerOptions) {\n const {\n model,\n systemPrompt = 'You are a helpful admin assistant for the Env Hopper application. Help users manage apps, data sources, and MCP server configurations.',\n tools = {},\n validateConfig,\n } = options\n\n return async (req: Request, res: Response) => {\n try {\n // Validate configuration if validator provided\n if (validateConfig) {\n validateConfig()\n }\n\n const { messages } = req.body as { messages: Array<UIMessageInput> }\n const coreMessages = convertToCoreMessages(messages)\n\n console.log(\n '[Admin Chat] Received messages:',\n JSON.stringify(coreMessages, null, 2),\n )\n console.log('[Admin Chat] Available tools:', Object.keys(tools))\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n tools,\n // Allow up to 5 steps so the model can call tools and then generate a response\n stopWhen: stepCountIs(5),\n onFinish: (event) => {\n console.log('[Admin Chat] Finished:', {\n finishReason: event.finishReason,\n usage: event.usage,\n hasText: !!event.text,\n textLength: event.text.length,\n })\n },\n })\n\n // Use UI message stream response which is compatible with AI SDK React hooks\n const response = result.toUIMessageStreamResponse()\n\n // Copy headers from the response\n response.headers.forEach((value, key) => {\n res.setHeader(key, value)\n })\n\n // Pipe the stream to the response\n if (response.body) {\n const reader = response.body.getReader()\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read()\n if (done) {\n res.end()\n return\n }\n res.write(value)\n return pump()\n }\n await pump()\n } else {\n console.error('[Admin Chat] No response body')\n res.status(500).json({ error: 'No response from AI model' })\n }\n } catch (error) {\n console.error('[Admin Chat] Error:', error)\n res.status(500).json({ error: 'Failed to process chat request' })\n }\n }\n}\n\n// Re-export tool helper for convenience\nexport { tool }\n","import type { Tool } from 'ai'\nimport { z } from 'zod'\nimport { getDbClient } from '../../../db'\n\n/**\n * Generic interface for executing raw SQL queries.\n * Can be implemented with Prisma's $queryRawUnsafe or any other SQL client.\n */\nexport interface DatabaseClient {\n /** Execute a SELECT query and return results */\n query: <T = unknown>(sql: string) => Promise<Array<T>>\n /** Execute an INSERT/UPDATE/DELETE and return affected row count */\n execute: (sql: string) => Promise<{ affectedRows: number }>\n /** Get list of tables in the database */\n getTables: () => Promise<Array<string>>\n /** Get columns for a specific table */\n getColumns: (\n tableName: string,\n ) => Promise<Array<{ name: string; type: string; nullable: boolean }>>\n}\n\n/**\n * Creates a DatabaseClient from a Prisma client.\n */\nexport function createPrismaDatabaseClient(prisma: {\n $queryRawUnsafe: <T>(sql: string) => Promise<T>\n $executeRawUnsafe: (sql: string) => Promise<number>\n}): DatabaseClient {\n return {\n query: async <T = unknown>(sql: string): Promise<Array<T>> => {\n const result = await prisma.$queryRawUnsafe<Array<T>>(sql)\n return result\n },\n execute: async (sql: string) => {\n const affectedRows = await prisma.$executeRawUnsafe(sql)\n return { affectedRows }\n },\n getTables: async () => {\n const tables = await prisma.$queryRawUnsafe<Array<{ tablename: string }>>(\n `SELECT tablename FROM pg_tables WHERE schemaname = 'public'`,\n )\n return tables.map((t) => t.tablename)\n },\n getColumns: async (tableName: string) => {\n const columns = await prisma.$queryRawUnsafe<\n Array<{\n column_name: string\n data_type: string\n is_nullable: string\n }>\n >(\n `SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}' AND table_schema = 'public'`,\n )\n return columns.map((c) => ({\n name: c.column_name,\n type: c.data_type,\n nullable: c.is_nullable === 'YES',\n }))\n },\n }\n}\n\n// Define zod schemas for tool parameters\nconst querySchema = z.object({\n sql: z.string().describe('The SELECT SQL query to execute'),\n})\n\nconst modifySchema = z.object({\n sql: z\n .string()\n .describe('The INSERT, UPDATE, or DELETE SQL query to execute'),\n confirmed: z\n .boolean()\n .describe('Must be true to execute destructive operations'),\n})\n\nconst schemaParamsSchema = z.object({\n tableName: z\n .string()\n .optional()\n .describe(\n 'Specific table name to get columns for. If not provided, returns list of all tables.',\n ),\n})\n\ntype QueryInput = z.infer<typeof querySchema>\ntype ModifyInput = z.infer<typeof modifySchema>\ntype SchemaInput = z.infer<typeof schemaParamsSchema>\n\n/**\n * Creates a DatabaseClient using the internal backend-core Prisma client.\n * This is a convenience function for apps that don't need to pass their own Prisma client.\n */\nfunction createInternalDatabaseClient(): DatabaseClient {\n return createPrismaDatabaseClient(getDbClient())\n}\n\n/**\n * Creates AI tools for generic database access.\n *\n * The AI uses these internally - users interact via natural language.\n * Results are formatted as tables by the AI based on the system prompt.\n * Uses the internal backend-core Prisma client automatically.\n */\nexport function createDatabaseTools(): Record<string, Tool> {\n const db = createInternalDatabaseClient()\n const queryDatabase: Tool<QueryInput, unknown> = {\n description: `Execute a SELECT query to read data from the database.\nUse this to list, search, or filter records from any table.\nAlways use double quotes around table and column names for PostgreSQL (e.g., SELECT * FROM \"App\").\nReturn results will be formatted as a table for the user.`,\n inputSchema: querySchema,\n execute: async ({ sql }) => {\n console.log(`Executing ${sql}`)\n // Safety check - only allow SELECT\n const normalizedSql = sql.trim().toUpperCase()\n if (!normalizedSql.startsWith('SELECT')) {\n return {\n error:\n 'Only SELECT queries are allowed with queryDatabase. Use modifyDatabase for changes.',\n }\n }\n try {\n const results = await db.query(sql)\n return {\n success: true,\n rowCount: Array.isArray(results) ? results.length : 0,\n data: results,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Query failed',\n }\n }\n },\n }\n\n const modifyDatabase: Tool<ModifyInput, unknown> = {\n description: `Execute an INSERT, UPDATE, or DELETE query to modify data.\nUse double quotes around table and column names for PostgreSQL.\nIMPORTANT: Always ask for user confirmation before executing. Set confirmed=true only after user confirms.\nFor UPDATE/DELETE, always include a WHERE clause to avoid affecting all rows.`,\n inputSchema: modifySchema,\n execute: async ({ sql, confirmed }) => {\n if (!confirmed) {\n return {\n needsConfirmation: true,\n message: 'Please confirm you want to execute this operation.',\n sql,\n }\n }\n\n // Safety check - don't allow SELECT here\n const normalizedSql = sql.trim().toUpperCase()\n if (normalizedSql.startsWith('SELECT')) {\n return { error: 'Use queryDatabase for SELECT queries.' }\n }\n\n // Extra safety - warn about missing WHERE on UPDATE/DELETE\n if (\n (normalizedSql.startsWith('UPDATE') ||\n normalizedSql.startsWith('DELETE')) &&\n !normalizedSql.includes('WHERE')\n ) {\n return {\n error:\n 'UPDATE and DELETE queries must include a WHERE clause for safety.',\n sql,\n }\n }\n\n try {\n const result = await db.execute(sql)\n return {\n success: true,\n affectedRows: result.affectedRows,\n message: `Operation completed. ${result.affectedRows} row(s) affected.`,\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Operation failed',\n }\n }\n },\n }\n\n const getDatabaseSchema: Tool<SchemaInput, unknown> = {\n description: `Get information about database tables and their columns.\nUse this to understand the database structure before writing queries.\nCall without tableName to list all tables, or with tableName to get columns for a specific table.`,\n inputSchema: schemaParamsSchema,\n execute: async ({ tableName }) => {\n try {\n if (tableName) {\n const columns = await db.getColumns(tableName)\n return {\n success: true,\n table: tableName,\n columns,\n }\n } else {\n const tables = await db.getTables()\n return {\n success: true,\n tables,\n }\n }\n } catch (error) {\n return {\n success: false,\n error:\n error instanceof Error ? error.message : 'Failed to get schema',\n }\n }\n },\n }\n\n return {\n queryDatabase,\n modifyDatabase,\n getDatabaseSchema,\n }\n}\n\n/**\n * Default system prompt for the database admin assistant.\n * Can be customized or extended.\n */\nexport const DEFAULT_ADMIN_SYSTEM_PROMPT = `You are a helpful database admin assistant. You help users view and manage data in the database.\n\nIMPORTANT RULES:\n1. When showing data, ALWAYS format it as a numbered ASCII table so users can reference rows by number\n2. NEVER show raw SQL to users - just describe what you're doing in plain language\n3. When users ask to modify data (update, delete, create), ALWAYS confirm before executing\n4. For updates, show the current value and ask for confirmation before changing\n5. Keep responses concise and focused on the data\n\nFORMATTING EXAMPLE:\nWhen user asks \"show me all apps\", respond like:\n\"Here are all the apps:\n\n| # | ID | Slug | Display Name | Icon |\n|---|----|---------|-----------------| -------|\n| 1 | 1 | portal | Portal | portal |\n| 2 | 2 | admin | Admin Dashboard | admin |\n| 3 | 3 | api | API Service | null |\n\nFound 3 apps total.\"\n\nWhen user says \"update row 2 display name to 'Admin Panel'\":\n1. First confirm: \"I'll update the app 'Admin Dashboard' (ID: 2) to have display name 'Admin Panel'. Proceed?\"\n2. Only after user confirms, execute the update\n3. Then show the updated row\n\nAVAILABLE TABLES:\nUse getDatabaseSchema tool to discover tables and their columns.\n`\n","import type { Request, Response, Router } from 'express'\nimport multer from 'multer'\nimport { createHash } from 'node:crypto'\nimport { getDbClient } from '../../db'\n\n// Configure multer for memory storage\nconst upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: 10 * 1024 * 1024, // 10MB limit\n },\n fileFilter: (_req, file, cb) => {\n // Accept images only\n if (!file.mimetype.startsWith('image/')) {\n cb(new Error('Only image files are allowed'))\n return\n }\n cb(null, true)\n },\n})\n\nexport interface IconRestControllerConfig {\n /**\n * Base path for icon endpoints (e.g., '/api/icons')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for icon upload and retrieval\n *\n * Endpoints:\n * - POST {basePath}/upload - Upload a new icon (multipart/form-data with 'icon' field and 'name' field)\n * - GET {basePath}/:id - Get icon binary by ID\n * - GET {basePath}/:id/metadata - Get icon metadata only\n */\nexport function registerIconRestController(\n router: Router,\n config: IconRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Upload endpoint - accepts multipart/form-data\n router.post(\n `${basePath}/upload`,\n upload.single('icon'),\n async (req: Request, res: Response) => {\n try {\n if (!req.file) {\n res.status(400).json({ error: 'No file uploaded' })\n return\n }\n\n const name = req.body['name'] as string\n if (!name) {\n res.status(400).json({ error: 'Name is required' })\n return\n }\n\n const prisma = getDbClient()\n const checksum = createHash('sha256')\n .update(req.file.buffer)\n .digest('hex')\n const icon = await prisma.dbAsset.create({\n data: {\n name,\n assetType: 'icon',\n content: new Uint8Array(req.file.buffer),\n mimeType: req.file.mimetype,\n fileSize: req.file.size,\n checksum,\n },\n })\n\n res.status(201).json({\n id: icon.id,\n name: icon.name,\n mimeType: icon.mimeType,\n fileSize: icon.fileSize,\n createdAt: icon.createdAt,\n })\n } catch (error) {\n console.error('Error uploading icon:', error)\n res.status(500).json({ error: 'Failed to upload icon' })\n }\n },\n )\n\n // Get icon binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', icon.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${icon.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(icon.content)\n } catch (error) {\n console.error('Error fetching icon:', error)\n res.status(500).json({ error: 'Failed to fetch icon' })\n }\n })\n\n // Get icon metadata only (no binary content)\n router.get(\n `${basePath}/:id/metadata`,\n async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n res.json(icon)\n } catch (error) {\n console.error('Error fetching icon metadata:', error)\n res.status(500).json({ error: 'Failed to fetch icon metadata' })\n }\n },\n )\n\n // Get icon binary by name\n router.get(\n `${basePath}/by-name/:name`,\n async (req: Request, res: Response) => {\n try {\n const { name } = req.params\n\n const prisma = getDbClient()\n const icon = await prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!icon) {\n res.status(404).json({ error: 'Icon not found' })\n return\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', icon.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${icon.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(icon.content)\n } catch (error) {\n console.error('Error fetching icon by name:', error)\n res.status(500).json({ error: 'Failed to fetch icon' })\n }\n },\n )\n}\n","import { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from '../assets/assetUtils'\n\nexport interface UpsertIconInput {\n name: string\n content: Buffer\n mimeType: string\n fileSize: number\n}\n\n/**\n * Upsert an icon to the database.\n * If an icon with the same name exists, it will be updated.\n * Otherwise, a new icon will be created.\n */\nexport async function upsertIcon(input: UpsertIconInput) {\n const prisma = getDbClient()\n \n const checksum = generateChecksum(input.content)\n const { width, height } = await getImageDimensions(input.content)\n \n return prisma.dbAsset.upsert({\n where: { name: input.name },\n update: {\n content: new Uint8Array(input.content),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n create: {\n name: input.name,\n assetType: 'icon',\n content: new Uint8Array(input.content),\n checksum,\n mimeType: input.mimeType,\n fileSize: input.fileSize,\n width,\n height,\n },\n })\n}\n\n/**\n * Upsert multiple icons to the database.\n * This is more efficient than calling upsertIcon multiple times.\n */\nexport async function upsertIcons(icons: Array<UpsertIconInput>) {\n const results = []\n for (const icon of icons) {\n const result = await upsertIcon(icon)\n results.push(result)\n }\n return results\n}\n\n/**\n * Get an asset (icon or screenshot) by name from the database.\n * Returns the asset content, mimeType, and name if found.\n */\nexport async function getAssetByName(name: string) {\n const prisma = getDbClient()\n \n return prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n}\n","import type { AssetType } from '@prisma/client'\nimport type { Request, Response, Router } from 'express'\nimport multer from 'multer'\nimport sharp from 'sharp'\nimport { getDbClient } from '../../db'\nimport {\n generateChecksum,\n getImageDimensions,\n getImageFormat,\n isRasterImage,\n resizeImage,\n} from './assetUtils'\n\n// Configure multer for memory storage\nconst upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: 10 * 1024 * 1024, // 10MB limit\n },\n fileFilter: (_req, file, cb) => {\n // Accept images only\n if (!file.mimetype.startsWith('image/')) {\n cb(new Error('Only image files are allowed'))\n return\n }\n cb(null, true)\n },\n})\n\nexport interface AssetRestControllerConfig {\n /**\n * Base path for asset endpoints (e.g., '/api/assets')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for universal asset upload and retrieval\n *\n * Endpoints:\n * - POST {basePath}/upload - Upload a new asset (multipart/form-data)\n * - GET {basePath}/:id - Get asset binary by ID\n * - GET {basePath}/:id/metadata - Get asset metadata only\n * - GET {basePath}/by-name/:name - Get asset binary by name\n */\nexport function registerAssetRestController(\n router: Router,\n config: AssetRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Upload endpoint - accepts multipart/form-data\n router.post(\n `${basePath}/upload`,\n upload.single('asset'),\n async (req: Request, res: Response) => {\n try {\n if (!req.file) {\n res.status(400).json({ error: 'No file uploaded' })\n return\n }\n\n const name = req.body['name'] as string\n const assetTypeInput = req.body['assetType']\n const assetType = (assetTypeInput as AssetType | undefined) ?? 'icon'\n\n if (!name) {\n res.status(400).json({ error: 'Name is required' })\n return\n }\n\n const prisma = getDbClient()\n\n // Compute checksum of the binary for content-based deduplication.\n const checksum = generateChecksum(req.file.buffer)\n\n // If an asset with the same checksum already exists, reuse it instead of storing duplicate binary.\n const existing = await prisma.dbAsset.findUnique({\n where: { checksum },\n select: {\n id: true,\n name: true,\n assetType: true,\n checksum: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n if (existing) {\n res.status(200).json(existing)\n return\n }\n\n // Get image dimensions using our utility\n const { width, height } = await getImageDimensions(req.file.buffer)\n\n const asset = await prisma.dbAsset.create({\n data: {\n name,\n checksum,\n assetType,\n content: new Uint8Array(req.file.buffer),\n mimeType: req.file.mimetype,\n fileSize: req.file.size,\n width,\n height,\n },\n })\n\n res.status(201).json({\n id: asset.id,\n name: asset.name,\n assetType: asset.assetType,\n mimeType: asset.mimeType,\n fileSize: asset.fileSize,\n width: asset.width,\n height: asset.height,\n createdAt: asset.createdAt,\n })\n } catch (error) {\n console.error('Error uploading asset:', error)\n res.status(500).json({ error: 'Failed to upload asset' })\n }\n },\n )\n\n // Get asset binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n width: true,\n height: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n const resizeEnabled =\n String(process.env.EH_ASSETS_RESIZE_ENABLED || 'true') === 'true'\n const wParam = req.query['w'] as string | undefined\n const width = wParam ? Number.parseInt(wParam, 10) : undefined\n\n let outBuffer: Uint8Array = asset.content\n let outMime = asset.mimeType\n\n const shouldResize =\n resizeEnabled &&\n isRasterImage(asset.mimeType) &&\n !!width &&\n Number.isFinite(width) &&\n width > 0\n\n if (shouldResize) {\n const fmt = getImageFormat(asset.mimeType) || 'jpeg'\n const buf = await resizeImage(\n Buffer.from(asset.content),\n width,\n undefined,\n fmt,\n )\n outBuffer = new Uint8Array(buf)\n outMime = `image/${fmt}`\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', outMime)\n res.setHeader('Content-Disposition', `inline; filename=\"${asset.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content (resized if requested)\n res.send(outBuffer)\n } catch (error) {\n console.error('Error fetching asset:', error)\n res.status(500).json({ error: 'Failed to fetch asset' })\n }\n })\n\n // Get asset metadata only (no binary content)\n router.get(\n `${basePath}/:id/metadata`,\n async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n assetType: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n res.json(asset)\n } catch (error) {\n console.error('Error fetching asset metadata:', error)\n res.status(500).json({ error: 'Failed to fetch asset metadata' })\n }\n },\n )\n\n // Get asset binary by name\n router.get(\n `${basePath}/by-name/:name`,\n async (req: Request, res: Response) => {\n try {\n const { name } = req.params\n\n const prisma = getDbClient()\n const asset = await prisma.dbAsset.findUnique({\n where: { name },\n select: {\n content: true,\n mimeType: true,\n name: true,\n width: true,\n height: true,\n },\n })\n\n if (!asset) {\n res.status(404).json({ error: 'Asset not found' })\n return\n }\n\n const resizeEnabled =\n String(process.env.EH_ASSETS_RESIZE_ENABLED || 'true') === 'true'\n const wParam = req.query['w'] as string | undefined\n const width = wParam ? Number.parseInt(wParam, 10) : undefined\n\n let outBuffer: Uint8Array = asset.content\n let outMime = asset.mimeType\n\n const isRaster =\n asset.mimeType.startsWith('image/') && !asset.mimeType.includes('svg')\n const shouldResize =\n resizeEnabled &&\n isRaster &&\n !!width &&\n Number.isFinite(width) &&\n width > 0\n\n if (shouldResize) {\n const fmt = asset.mimeType.includes('png')\n ? 'png'\n : asset.mimeType.includes('webp')\n ? 'webp'\n : 'jpeg'\n\n let buf: Buffer\n const pipeline = sharp(Buffer.from(asset.content)).resize({\n width,\n fit: 'inside',\n withoutEnlargement: true,\n })\n if (fmt === 'png') {\n buf = await pipeline.png().toBuffer()\n outMime = 'image/png'\n } else if (fmt === 'webp') {\n buf = await pipeline.webp().toBuffer()\n outMime = 'image/webp'\n } else {\n buf = await pipeline.jpeg().toBuffer()\n outMime = 'image/jpeg'\n }\n outBuffer = new Uint8Array(buf)\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', outMime)\n res.setHeader('Content-Disposition', `inline; filename=\"${asset.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content (resized if requested)\n res.send(outBuffer)\n } catch (error) {\n console.error('Error fetching asset by name:', error)\n res.status(500).json({ error: 'Failed to fetch asset' })\n }\n },\n )\n}\n","import type { Request, Response, Router } from 'express'\nimport sharp from 'sharp'\nimport { getDbClient } from '../../db'\n\nexport interface ScreenshotRestControllerConfig {\n /**\n * Base path for screenshot endpoints (e.g., '/api/screenshots')\n */\n basePath: string\n}\n\n/**\n * Registers REST endpoints for screenshot retrieval\n * \n * Endpoints:\n * - GET {basePath}/app/:appId - Get all screenshots for an app\n * - GET {basePath}/:id - Get screenshot binary by ID\n * - GET {basePath}/:id/metadata - Get screenshot metadata only\n */\nexport function registerScreenshotRestController(\n router: Router,\n config: ScreenshotRestControllerConfig,\n): void {\n const { basePath } = config\n\n // Get all screenshots for an app\n router.get(`${basePath}/app/:appSlug`, async (req: Request, res: Response) => {\n try {\n const { appSlug } = req.params\n\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app) {\n res.status(404).json({ error: 'App not found' })\n return\n }\n\n // Fetch all screenshots for the app\n const screenshots = await prisma.dbAsset.findMany({\n where: {\n id: { in: app.screenshotIds },\n assetType: 'screenshot',\n },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n res.json(screenshots)\n } catch (error) {\n console.error('Error fetching app screenshots:', error)\n res.status(500).json({ error: 'Failed to fetch screenshots' })\n }\n })\n\n // Get first screenshot for an app (convenience endpoint)\n router.get(`${basePath}/app/:appSlug/first`, async (req: Request, res: Response) => {\n try {\n const { appSlug } = req.params\n\n const prisma = getDbClient()\n \n // Find app by slug\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appSlug },\n select: { screenshotIds: true },\n })\n\n if (!app || app.screenshotIds.length === 0) {\n res.status(404).json({ error: 'No screenshots found' })\n return\n }\n\n // Fetch first screenshot\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id: app.screenshotIds[0] },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n res.json(screenshot)\n } catch (error) {\n console.error('Error fetching first screenshot:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot' })\n }\n })\n\n // Get screenshot binary by ID\n router.get(`${basePath}/:id`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n const sizeParam = req.query.size as string | undefined\n const targetSize = sizeParam ? parseInt(sizeParam, 10) : undefined\n\n const prisma = getDbClient()\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n content: true,\n mimeType: true,\n name: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n let content: Uint8Array | Buffer = screenshot.content\n\n // Resize if size parameter provided\n if (targetSize && targetSize > 0) {\n try {\n content = await sharp(screenshot.content)\n .resize(targetSize, targetSize, {\n fit: 'inside',\n withoutEnlargement: true,\n })\n .toBuffer()\n } catch (resizeError) {\n console.error('Error resizing screenshot:', resizeError)\n // Fall back to original if resize fails\n }\n }\n\n // Set appropriate headers\n res.setHeader('Content-Type', screenshot.mimeType)\n res.setHeader('Content-Disposition', `inline; filename=\"${screenshot.name}\"`)\n res.setHeader('Cache-Control', 'public, max-age=86400') // Cache for 1 day\n\n // Send binary content\n res.send(content)\n } catch (error) {\n console.error('Error fetching screenshot:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot' })\n }\n })\n\n // Get screenshot metadata only (no binary content)\n router.get(`${basePath}/:id/metadata`, async (req: Request, res: Response) => {\n try {\n const { id } = req.params\n\n const prisma = getDbClient()\n const screenshot = await prisma.dbAsset.findUnique({\n where: { id },\n select: {\n id: true,\n name: true,\n mimeType: true,\n fileSize: true,\n width: true,\n height: true,\n createdAt: true,\n updatedAt: true,\n },\n })\n\n if (!screenshot) {\n res.status(404).json({ error: 'Screenshot not found' })\n return\n }\n\n res.json(screenshot)\n } catch (error) {\n console.error('Error fetching screenshot metadata:', error)\n res.status(500).json({ error: 'Failed to fetch screenshot metadata' })\n }\n })\n}\n","import { readFileSync, readdirSync } from 'node:fs'\nimport { extname, join } from 'node:path'\nimport { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from './assetUtils'\n\nexport interface SyncAssetsConfig {\n /**\n * Directory containing icon files to sync\n */\n iconsDir?: string\n\n /**\n * Directory containing screenshot files to sync\n */\n screenshotsDir?: string\n}\n\n/**\n * Sync local asset files (icons and screenshots) from directories into the database.\n * \n * This function allows consuming applications to sync asset files without directly\n * exposing the Prisma client. It handles:\n * - Icon files: Assigned to apps by matching filename to icon name patterns\n * - Screenshot files: Assigned to apps by matching filename to app ID (format: <app-id>_screenshot_<no>.<ext>)\n * \n * @param config Configuration with paths to icon and screenshot directories\n */\nexport async function syncAssets(config: SyncAssetsConfig): Promise<{\n iconsUpserted: number\n screenshotsUpserted: number\n}> {\n const prisma = getDbClient()\n let iconsUpserted = 0\n let screenshotsUpserted = 0\n\n // Sync icons from local/icons directory\n if (config.iconsDir) {\n console.log(`📁 Syncing icons from ${config.iconsDir}...`)\n iconsUpserted = await syncIconsFromDirectory(prisma, config.iconsDir)\n console.log(` ✓ Upserted ${iconsUpserted} icons`)\n }\n\n // Sync screenshots from local/screenshots directory\n if (config.screenshotsDir) {\n console.log(`📷 Syncing screenshots from ${config.screenshotsDir}...`)\n screenshotsUpserted = await syncScreenshotsFromDirectory(prisma, config.screenshotsDir)\n console.log(` ✓ Upserted ${screenshotsUpserted} screenshots and assigned to apps`)\n }\n\n return {\n iconsUpserted,\n screenshotsUpserted,\n }\n}\n\n/**\n * Sync icon files from a directory\n */\nasync function syncIconsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n iconsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(iconsDir)\n\n for (const file of files) {\n const filePath = join(iconsDir, file)\n const ext = extname(file).toLowerCase().slice(1) // Remove leading dot\n\n // Skip non-image files\n if (!['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n continue\n }\n\n try {\n const content = readFileSync(filePath)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n const iconName = file.replace(/\\.[^/.]+$/, '') // Remove extension\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n continue // Already synced\n }\n\n // Extract dimensions for raster images\n let width: number | null = null\n let height: number | null = null\n if (!ext.includes('svg')) {\n const { width: w, height: h } = await getImageDimensions(buffer)\n width = w\n height = h\n }\n\n // Determine MIME type\n const mimeType = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n }[ext] || 'application/octet-stream'\n\n await prisma.dbAsset.create({\n data: {\n name: iconName,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync icon ${file}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading icons directory:`, error)\n }\n\n return count\n}\n\n/**\n * Sync screenshot files from a directory and assign to apps\n */\nasync function syncScreenshotsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n screenshotsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(screenshotsDir)\n\n // Group screenshots by app ID\n const screenshotsByApp = new Map<string, Array<{ path: string; ext: string }>>()\n\n for (const file of files) {\n // Parse filename: <app-id>_screenshot_<no>.<ext>\n const match = file.match(/^(.+?)_screenshot_(\\d+)\\.([^.]+)$/)\n if (!match || !match[1] || !match[3]) {\n continue\n }\n\n const appId = match[1]\n const ext = match[3]\n if (!screenshotsByApp.has(appId)) {\n screenshotsByApp.set(appId, [])\n }\n screenshotsByApp.get(appId)!.push({\n path: join(screenshotsDir, file),\n ext,\n })\n }\n\n // Process each app's screenshots\n for (const [appId, screenshots] of screenshotsByApp) {\n try {\n // Check if app exists\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n select: { id: true },\n })\n\n if (!app) {\n console.warn(` ⚠ App not found: ${appId}`)\n continue\n }\n\n // Sync screenshots for this app\n for (const screenshot of screenshots) {\n try {\n const content = readFileSync(screenshot.path)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n\n // Check if screenshot with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'screenshot' },\n })\n\n if (existing) {\n // Link to app via screenshotIds array if not already linked\n const existingApp = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n })\n if (existingApp && !existingApp.screenshotIds.includes(existing.id)) {\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: [...existingApp.screenshotIds, existing.id],\n },\n })\n }\n continue\n }\n\n // Extract dimensions\n const { width, height } = await getImageDimensions(buffer)\n\n // Determine MIME type\n const mimeType = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n }[screenshot.ext.toLowerCase()] || 'application/octet-stream'\n\n // Create screenshot asset\n const asset = await prisma.dbAsset.create({\n data: {\n name: `${appId}-screenshot-${Date.now()}`,\n assetType: 'screenshot',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n // Link screenshot to app via screenshotIds array\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: {\n push: asset.id,\n },\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync screenshot ${screenshot.path}:`, error)\n }\n }\n } catch (error) {\n console.warn(` ⚠ Failed to process app ${appId}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading screenshots directory:`, error)\n }\n\n return count\n}\n","import { PrismaClient } from '@prisma/client'\nimport type { EhDatabaseConfig } from './types'\nimport { setDbClient } from '../db/client'\n\n/**\n * Formats a database connection URL from structured config.\n */\nfunction formatConnectionUrl(config: EhDatabaseConfig): string {\n if ('url' in config) {\n return config.url\n }\n\n const { host, port, database, username, password, schema = 'public' } = config\n return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`\n}\n\n/**\n * Internal database manager used by the middleware.\n * Handles connection URL formatting and lifecycle.\n */\nexport class EhDatabaseManager {\n private client: PrismaClient | null = null\n private config: EhDatabaseConfig\n\n constructor(config: EhDatabaseConfig) {\n this.config = config\n }\n\n /**\n * Get or create the Prisma client instance.\n * Uses lazy initialization for flexibility.\n */\n getClient(): PrismaClient {\n if (!this.client) {\n const datasourceUrl = formatConnectionUrl(this.config)\n\n this.client = new PrismaClient({\n datasourceUrl,\n log:\n process.env.NODE_ENV === 'development'\n ? ['warn', 'error']\n : ['warn', 'error'],\n })\n\n // Bridge with existing backend-core getDbClient() usage\n setDbClient(this.client)\n }\n return this.client\n }\n\n async connect(): Promise<void> {\n const client = this.getClient()\n await client.$connect()\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n await this.client.$disconnect()\n this.client = null\n }\n }\n}\n","import type { EhBackendCompanySpecificBackend } from '../types/backend/companySpecificBackend'\nimport type { EhBackendProvider } from './types'\n\n/**\n * Type guard to check if an object implements EhBackendCompanySpecificBackend.\n */\nfunction isBackendInstance(obj: unknown): obj is EhBackendCompanySpecificBackend {\n return (\n typeof obj === 'object' &&\n obj !== null &&\n typeof (obj as EhBackendCompanySpecificBackend).getBootstrapData === 'function' &&\n typeof (obj as EhBackendCompanySpecificBackend).getAvailabilityMatrix ===\n 'function' &&\n typeof (obj as EhBackendCompanySpecificBackend).getNameMigrations ===\n 'function' &&\n typeof (obj as EhBackendCompanySpecificBackend).getResourceJumps ===\n 'function' &&\n typeof (obj as EhBackendCompanySpecificBackend).getResourceJumpsExtended ===\n 'function'\n )\n}\n\n/**\n * Normalizes different backend provider types into a consistent async factory function.\n * Supports:\n * - Direct object implementing EhBackendCompanySpecificBackend\n * - Sync factory function that returns the backend\n * - Async factory function that returns the backend\n */\nexport function createBackendResolver(\n provider: EhBackendProvider,\n): () => Promise<EhBackendCompanySpecificBackend> {\n // If it's already an object with the required methods, wrap it\n if (isBackendInstance(provider)) {\n return async () => provider\n }\n\n // If it's a function, call it and handle both sync and async results\n if (typeof provider === 'function') {\n return async () => {\n const result = provider()\n return result instanceof Promise ? result : result\n }\n }\n\n throw new Error(\n 'Invalid backend provider: must be an object implementing EhBackendCompanySpecificBackend or a factory function',\n )\n}\n","import type { Router } from 'express'\nimport { toNodeHandler } from 'better-auth/node'\nimport type {\n EhFeatureToggles,\n EhMiddlewareOptions,\n MiddlewareContext,\n} from './types'\nimport { registerIconRestController } from '../modules/icons/iconRestController'\nimport { registerAssetRestController } from '../modules/assets/assetRestController'\nimport { registerScreenshotRestController } from '../modules/assets/screenshotRestController'\nimport { createAdminChatHandler } from '../modules/admin/chat/createAdminChatHandler'\nimport { getAssetByName } from '../modules/icons/iconService'\n\ninterface FeatureRegistration {\n name: keyof EhFeatureToggles\n defaultEnabled: boolean\n register: (\n router: Router,\n options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &\n EhMiddlewareOptions,\n context: MiddlewareContext,\n ) => void\n}\n\nconst FEATURES: Array<FeatureRegistration> = [\n {\n name: 'auth',\n defaultEnabled: true,\n register: (router, options, ctx) => {\n const basePath = options.basePath\n\n // Explicit session endpoint handler\n router.get(`${basePath}/auth/session`, async (req, res) => {\n try {\n const session = await ctx.auth.api.getSession({\n headers: req.headers as HeadersInit,\n })\n if (session) {\n res.json(session)\n } else {\n res.status(401).json({ error: 'Not authenticated' })\n }\n } catch (error) {\n console.error('[Auth Session Error]', error)\n res.status(500).json({ error: 'Internal server error' })\n }\n })\n\n // Use toNodeHandler to adapt better-auth for Express/Node.js\n const authHandler = toNodeHandler(ctx.auth)\n router.all(`${basePath}/auth/{*any}`, authHandler)\n },\n },\n {\n name: 'icons',\n defaultEnabled: true,\n register: (router, options) => {\n registerIconRestController(router, {\n basePath: `${options.basePath}/icons`,\n })\n },\n },\n {\n name: 'assets',\n defaultEnabled: true,\n register: (router, options) => {\n registerAssetRestController(router, {\n basePath: `${options.basePath}/assets`,\n })\n },\n },\n {\n name: 'screenshots',\n defaultEnabled: true,\n register: (router, options) => {\n registerScreenshotRestController(router, {\n basePath: `${options.basePath}/screenshots`,\n })\n },\n },\n {\n name: 'adminChat',\n defaultEnabled: false, // Only enabled if adminChat config is provided\n register: (router, options) => {\n if (options.adminChat) {\n router.post(\n `${options.basePath}/admin/chat`,\n createAdminChatHandler(options.adminChat),\n )\n }\n },\n },\n {\n name: 'legacyIconEndpoint',\n defaultEnabled: false,\n register: (router) => {\n // Legacy endpoint at /static/icon/:icon for backwards compatibility\n router.get('/static/icon/:icon', async (req, res) => {\n const { icon } = req.params\n\n if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {\n res.status(400).send('Invalid icon name')\n return\n }\n\n try {\n const dbIcon = await getAssetByName(icon)\n\n if (!dbIcon) {\n res.status(404).send('Icon not found')\n return\n }\n\n res.setHeader('Content-Type', dbIcon.mimeType)\n res.setHeader('Cache-Control', 'public, max-age=86400')\n res.send(dbIcon.content)\n } catch (error) {\n console.error('Error fetching icon:', error)\n res.status(404).send('Icon not found')\n }\n })\n },\n },\n]\n\n/**\n * Registers all enabled features on the router.\n */\nexport function registerFeatures(\n router: Router,\n options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &\n EhMiddlewareOptions,\n context: MiddlewareContext,\n): void {\n const toggles = options.features || {}\n\n for (const feature of FEATURES) {\n const isEnabled = toggles[feature.name] ?? feature.defaultEnabled\n\n // Special case: adminChat is only enabled if config is provided\n if (feature.name === 'adminChat' && !options.adminChat) {\n continue\n }\n\n if (isEnabled) {\n feature.register(router, options, context)\n }\n }\n}\n","import express, { Router } from 'express'\nimport * as trpcExpress from '@trpc/server/adapters/express'\nimport type { EhMiddlewareOptions, EhMiddlewareResult, MiddlewareContext } from './types'\nimport { EhDatabaseManager } from './database'\nimport { createBackendResolver } from './backendResolver'\nimport { registerFeatures } from './featureRegistry'\nimport { createTrpcRouter } from '../server/controller'\nimport { createEhTrpcContext } from '../server/ehTrpcContext'\nimport { createAuth } from '../modules/auth/auth'\n\n/**\n * Creates a fully-configured env-hopper middleware.\n *\n * @example\n * ```typescript\n * // Simple usage with inline backend\n * const eh = await createEhMiddleware({\n * basePath: '/api',\n * database: { url: process.env.DATABASE_URL! },\n * auth: {\n * baseURL: 'http://localhost:4000',\n * secret: process.env.AUTH_SECRET!,\n * },\n * backend: {\n * getBootstrapData: async () => loadStaticData(),\n * getAvailabilityMatrix: async () => ({}),\n * getNameMigrations: async () => false,\n * getResourceJumps: async () => ({ resourceJumps: [], envs: [], lateResolvableParams: [] }),\n * getResourceJumpsExtended: async () => ({ envs: [] }),\n * },\n * })\n *\n * app.use(eh.router)\n * await eh.connect()\n * ```\n *\n * @example\n * ```typescript\n * // With DI-resolved backend (e.g., tsyringe)\n * const eh = await createEhMiddleware({\n * basePath: '/api',\n * database: {\n * host: cfg.db.host,\n * port: cfg.db.port,\n * database: cfg.db.name,\n * username: cfg.db.username,\n * password: cfg.db.password,\n * schema: cfg.db.schema,\n * },\n * auth: {\n * baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:4000',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * providers: getAuthProvidersFromEnv(),\n * plugins: getAuthPluginsFromEnv(),\n * },\n * // Factory function - resolved fresh per request from DI container\n * backend: () => container.resolve(NateraEhBackend),\n * hooks: {\n * onRoutesRegistered: (router) => {\n * router.get('/health', (_, res) => res.send('ok'))\n * },\n * },\n * })\n * ```\n */\nexport async function createEhMiddleware(\n options: EhMiddlewareOptions,\n): Promise<EhMiddlewareResult> {\n // Normalize options with defaults\n const basePath = options.basePath ?? '/api'\n const normalizedOptions = { ...options, basePath }\n\n // Check if database-dependent features are enabled\n const features = options.features ?? {}\n const iconsEnabled = features.icons !== false\n const assetsEnabled = features.assets !== false\n const screenshotsEnabled = features.screenshots !== false\n const legacyIconEnabled = features.legacyIconEndpoint === true\n const needsDatabase = iconsEnabled || assetsEnabled || screenshotsEnabled || legacyIconEnabled\n\n // Validate database is provided when needed\n if (needsDatabase && !options.database) {\n throw new Error(\n 'Database configuration is required when icons, assets, screenshots, or legacyIconEndpoint features are enabled. ' +\n 'Either provide database config or disable these features.',\n )\n }\n\n // Initialize database manager only if database config is provided\n let dbManager: EhDatabaseManager | null = null\n if (options.database) {\n dbManager = new EhDatabaseManager(options.database)\n // Initialize the client (which also sets the global singleton)\n dbManager.getClient()\n }\n\n // Create auth instance\n const auth = createAuth({\n appName: options.auth.appName,\n baseURL: options.auth.baseURL,\n secret: options.auth.secret,\n providers: options.auth.providers,\n plugins: options.auth.plugins,\n sessionExpiresIn: options.auth.sessionExpiresIn,\n sessionUpdateAge: options.auth.sessionUpdateAge,\n })\n\n // Create tRPC router\n const trpcRouter = createTrpcRouter(auth)\n\n // Normalize backend provider to async factory function\n const resolveBackend = createBackendResolver(options.backend)\n\n // Create tRPC context factory\n const createContext = async () => {\n const companySpecificBackend = await resolveBackend()\n return createEhTrpcContext({ companySpecificBackend })\n }\n\n // Create Express router\n const router = Router()\n router.use(express.json())\n\n // Build middleware context for feature registration\n const middlewareContext: MiddlewareContext = {\n auth,\n trpcRouter,\n createContext,\n }\n\n // Register tRPC middleware (if enabled)\n if (normalizedOptions.features?.trpc !== false) {\n router.use(\n `${basePath}/trpc`,\n trpcExpress.createExpressMiddleware({\n router: trpcRouter,\n createContext,\n }),\n )\n }\n\n // Register all enabled features\n registerFeatures(router, normalizedOptions, middlewareContext)\n\n // Call onRoutesRegistered hook if provided\n if (options.hooks?.onRoutesRegistered) {\n await options.hooks.onRoutesRegistered(router)\n }\n\n return {\n router,\n auth,\n trpcRouter,\n\n async connect(): Promise<void> {\n if (dbManager) {\n await dbManager.connect()\n }\n if (options.hooks?.onDatabaseConnected) {\n await options.hooks.onDatabaseConnected()\n }\n },\n\n async disconnect(): Promise<void> {\n if (options.hooks?.onDatabaseDisconnecting) {\n await options.hooks.onDatabaseDisconnecting()\n }\n if (dbManager) {\n await dbManager.disconnect()\n }\n },\n\n addRoutes(callback: (router: Router) => void): void {\n callback(router)\n },\n }\n}\n"],"mappings":"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"}
|