@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@env-hopper/backend-core",
3
- "version": "2.0.1-alpha.1",
3
+ "version": "2.0.1-alpha.3",
4
4
  "description": "Backend core library for Env Hopper",
5
5
  "homepage": "https://github.com/lislon/env-hopper",
6
6
  "repository": {
@@ -48,8 +48,8 @@
48
48
  "tsyringe": "^4.10.0",
49
49
  "yaml": "^2.8.0",
50
50
  "zod": "^4.3.5",
51
- "@env-hopper/shared-core": "2.0.1-alpha.1",
52
- "@env-hopper/table-sync": "2.0.1-alpha"
51
+ "@env-hopper/table-sync": "2.0.1-alpha.3",
52
+ "@env-hopper/shared-core": "2.0.1-alpha.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@tanstack/vite-config": "^0.4.3",
package/src/db/client.ts CHANGED
@@ -13,6 +13,14 @@ export function getDbClient(): PrismaClient {
13
13
  return prismaClient
14
14
  }
15
15
 
16
+ /**
17
+ * Sets the internal Prisma client instance.
18
+ * Used by middleware to bridge with existing getDbClient() usage.
19
+ */
20
+ export function setDbClient(client: PrismaClient): void {
21
+ prismaClient = client
22
+ }
23
+
16
24
  /**
17
25
  * Connects to the database.
18
26
  * Call this before performing database operations.
package/src/db/index.ts CHANGED
@@ -1,16 +1,20 @@
1
1
  // Database connection
2
- export { connectDb, disconnectDb, getDbClient } from './client'
2
+ export { connectDb, disconnectDb, getDbClient, setDbClient } from './client'
3
3
 
4
4
  // Table sync utilities
5
5
  export {
6
- tableSyncPrisma, type MakeTFromPrismaModel, type ObjectKeys,
7
- type ScalarFilter, type ScalarKeys, type TableSyncParamsPrisma
6
+ tableSyncPrisma,
7
+ type MakeTFromPrismaModel,
8
+ type ObjectKeys,
9
+ type ScalarFilter,
10
+ type ScalarKeys,
11
+ type TableSyncParamsPrisma,
8
12
  } from './tableSyncPrismaAdapter'
9
13
 
10
14
  export {
11
15
  TABLE_SYNC_MAGAZINE,
12
16
  type TableSyncMagazine,
13
- type TableSyncMagazineModelNameKey
17
+ type TableSyncMagazineModelNameKey,
14
18
  } from './tableSyncMagazine'
15
19
 
16
20
  // App catalog sync
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@ export { createTrpcRouter } from './server/controller'
4
4
  export type { TRPCRouter } from './server/controller'
5
5
  export { createEhTrpcContext } from './server/ehTrpcContext'
6
6
  export type {
7
- EhTrpcContext,
8
- EhTrpcContextOptions
7
+ EhTrpcContext,
8
+ EhTrpcContextOptions,
9
9
  } from './server/ehTrpcContext'
10
10
 
11
11
  export { staticControllerContract } from './server/ehStaticControllerContract'
@@ -20,92 +20,107 @@ export * from './types/index'
20
20
 
21
21
  // Auth
22
22
  export {
23
- createAuth,
24
- type AuthConfig,
25
- type BetterAuth
23
+ createAuth,
24
+ type AuthConfig,
25
+ type BetterAuth,
26
26
  } from './modules/auth/auth'
27
27
 
28
- export {
29
- registerAuthRoutes
30
- } from './modules/auth/registerAuthRoutes'
28
+ export { registerAuthRoutes } from './modules/auth/registerAuthRoutes'
31
29
 
32
- export {
33
- createAuthRouter,
34
- type AuthRouter
35
- } from './modules/auth/authRouter'
30
+ export { createAuthRouter, type AuthRouter } from './modules/auth/authRouter'
36
31
 
37
32
  export {
38
- getAuthProvidersFromEnv,
39
- getAuthPluginsFromEnv,
40
- validateAuthConfig
33
+ getAuthProvidersFromEnv,
34
+ getAuthPluginsFromEnv,
35
+ validateAuthConfig,
41
36
  } from './modules/auth/authProviders'
42
37
 
43
38
  export {
44
- getUserGroups,
45
- isMemberOfAnyGroup,
46
- isMemberOfAllGroups,
47
- getAdminGroupsFromEnv,
48
- isAdmin,
49
- requireAdmin,
50
- requireGroups,
51
- type UserWithGroups
39
+ getUserGroups,
40
+ isMemberOfAnyGroup,
41
+ isMemberOfAllGroups,
42
+ getAdminGroupsFromEnv,
43
+ isAdmin,
44
+ requireAdmin,
45
+ requireGroups,
46
+ type UserWithGroups,
52
47
  } from './modules/auth/authorizationUtils'
53
48
 
54
49
  // Admin
55
50
  export {
56
- createAdminChatHandler,
57
- tool,
58
- type AdminChatHandlerOptions
51
+ createAdminChatHandler,
52
+ tool,
53
+ type AdminChatHandlerOptions,
59
54
  } from './modules/admin/chat/createAdminChatHandler'
60
55
 
61
56
  export {
62
- createDatabaseTools,
63
- createPrismaDatabaseClient, DEFAULT_ADMIN_SYSTEM_PROMPT, type DatabaseClient
57
+ createDatabaseTools,
58
+ createPrismaDatabaseClient,
59
+ DEFAULT_ADMIN_SYSTEM_PROMPT,
60
+ type DatabaseClient,
64
61
  } from './modules/admin/chat/createDatabaseTools'
65
62
 
66
63
  // Icon management
67
64
  export {
68
- registerIconRestController,
69
- type IconRestControllerConfig
65
+ registerIconRestController,
66
+ type IconRestControllerConfig,
70
67
  } from './modules/icons/iconRestController'
71
68
 
72
69
  export {
73
- getAssetByName,
74
- upsertIcon,
75
- upsertIcons,
76
- type UpsertIconInput
70
+ getAssetByName,
71
+ upsertIcon,
72
+ upsertIcons,
73
+ type UpsertIconInput,
77
74
  } from './modules/icons/iconService'
78
75
 
79
76
  // Asset management (universal for icons, screenshots, etc.)
80
77
  export {
81
- registerAssetRestController,
82
- type AssetRestControllerConfig
78
+ registerAssetRestController,
79
+ type AssetRestControllerConfig,
83
80
  } from './modules/assets/assetRestController'
84
81
 
85
82
  export {
86
- registerScreenshotRestController,
87
- type ScreenshotRestControllerConfig
83
+ registerScreenshotRestController,
84
+ type ScreenshotRestControllerConfig,
88
85
  } from './modules/assets/screenshotRestController'
89
86
 
90
- export {
91
- createScreenshotRouter
92
- } from './modules/assets/screenshotRouter'
87
+ export { createScreenshotRouter } from './modules/assets/screenshotRouter'
93
88
 
94
- export {
95
- syncAssets,
96
- type SyncAssetsConfig
97
- } from './modules/assets/syncAssets'
89
+ export { syncAssets, type SyncAssetsConfig } from './modules/assets/syncAssets'
98
90
 
99
91
  // App Catalog Admin
100
- export {
101
- createAppCatalogAdminRouter
102
- } from './modules/appCatalogAdmin/appCatalogAdminRouter'
92
+ export { createAppCatalogAdminRouter } from './modules/appCatalogAdmin/appCatalogAdminRouter'
103
93
 
104
94
  // Database utilities
105
95
  export {
106
- connectDb,
107
- disconnectDb, getDbClient, syncAppCatalog, TABLE_SYNC_MAGAZINE, tableSyncPrisma, type MakeTFromPrismaModel, type ObjectKeys,
108
- type ScalarFilter, type ScalarKeys, type SyncAppCatalogResult, type TableSyncMagazine,
109
- type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma
96
+ connectDb,
97
+ disconnectDb,
98
+ getDbClient,
99
+ setDbClient,
100
+ syncAppCatalog,
101
+ TABLE_SYNC_MAGAZINE,
102
+ tableSyncPrisma,
103
+ type MakeTFromPrismaModel,
104
+ type ObjectKeys,
105
+ type ScalarFilter,
106
+ type ScalarKeys,
107
+ type SyncAppCatalogResult,
108
+ type TableSyncMagazine,
109
+ type TableSyncMagazineModelNameKey,
110
+ type TableSyncParamsPrisma,
110
111
  } from './db'
111
112
 
113
+ // Middleware (batteries-included backend setup)
114
+ export {
115
+ createEhMiddleware,
116
+ EhDatabaseManager,
117
+ type EhDatabaseConfig,
118
+ type EhAuthConfig,
119
+ type EhAdminChatConfig,
120
+ type EhFeatureToggles,
121
+ type EhBackendProvider,
122
+ type EhLifecycleHooks,
123
+ type EhMiddlewareOptions,
124
+ type EhMiddlewareResult,
125
+ type MiddlewareContext,
126
+ } from './middleware'
@@ -0,0 +1,49 @@
1
+ import type { EhBackendCompanySpecificBackend } from '../types/backend/companySpecificBackend'
2
+ import type { EhBackendProvider } from './types'
3
+
4
+ /**
5
+ * Type guard to check if an object implements EhBackendCompanySpecificBackend.
6
+ */
7
+ function isBackendInstance(obj: unknown): obj is EhBackendCompanySpecificBackend {
8
+ return (
9
+ typeof obj === 'object' &&
10
+ obj !== null &&
11
+ typeof (obj as EhBackendCompanySpecificBackend).getBootstrapData === 'function' &&
12
+ typeof (obj as EhBackendCompanySpecificBackend).getAvailabilityMatrix ===
13
+ 'function' &&
14
+ typeof (obj as EhBackendCompanySpecificBackend).getNameMigrations ===
15
+ 'function' &&
16
+ typeof (obj as EhBackendCompanySpecificBackend).getResourceJumps ===
17
+ 'function' &&
18
+ typeof (obj as EhBackendCompanySpecificBackend).getResourceJumpsExtended ===
19
+ 'function'
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Normalizes different backend provider types into a consistent async factory function.
25
+ * Supports:
26
+ * - Direct object implementing EhBackendCompanySpecificBackend
27
+ * - Sync factory function that returns the backend
28
+ * - Async factory function that returns the backend
29
+ */
30
+ export function createBackendResolver(
31
+ provider: EhBackendProvider,
32
+ ): () => Promise<EhBackendCompanySpecificBackend> {
33
+ // If it's already an object with the required methods, wrap it
34
+ if (isBackendInstance(provider)) {
35
+ return async () => provider
36
+ }
37
+
38
+ // If it's a function, call it and handle both sync and async results
39
+ if (typeof provider === 'function') {
40
+ return async () => {
41
+ const result = provider()
42
+ return result instanceof Promise ? result : result
43
+ }
44
+ }
45
+
46
+ throw new Error(
47
+ 'Invalid backend provider: must be an object implementing EhBackendCompanySpecificBackend or a factory function',
48
+ )
49
+ }
@@ -0,0 +1,177 @@
1
+ import express, { Router } from 'express'
2
+ import * as trpcExpress from '@trpc/server/adapters/express'
3
+ import type { EhMiddlewareOptions, EhMiddlewareResult, MiddlewareContext } from './types'
4
+ import { EhDatabaseManager } from './database'
5
+ import { createBackendResolver } from './backendResolver'
6
+ import { registerFeatures } from './featureRegistry'
7
+ import { createTrpcRouter } from '../server/controller'
8
+ import { createEhTrpcContext } from '../server/ehTrpcContext'
9
+ import { createAuth } from '../modules/auth/auth'
10
+
11
+ /**
12
+ * Creates a fully-configured env-hopper middleware.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Simple usage with inline backend
17
+ * const eh = await createEhMiddleware({
18
+ * basePath: '/api',
19
+ * database: { url: process.env.DATABASE_URL! },
20
+ * auth: {
21
+ * baseURL: 'http://localhost:4000',
22
+ * secret: process.env.AUTH_SECRET!,
23
+ * },
24
+ * backend: {
25
+ * getBootstrapData: async () => loadStaticData(),
26
+ * getAvailabilityMatrix: async () => ({}),
27
+ * getNameMigrations: async () => false,
28
+ * getResourceJumps: async () => ({ resourceJumps: [], envs: [], lateResolvableParams: [] }),
29
+ * getResourceJumpsExtended: async () => ({ envs: [] }),
30
+ * },
31
+ * })
32
+ *
33
+ * app.use(eh.router)
34
+ * await eh.connect()
35
+ * ```
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // With DI-resolved backend (e.g., tsyringe)
40
+ * const eh = await createEhMiddleware({
41
+ * basePath: '/api',
42
+ * database: {
43
+ * host: cfg.db.host,
44
+ * port: cfg.db.port,
45
+ * database: cfg.db.name,
46
+ * username: cfg.db.username,
47
+ * password: cfg.db.password,
48
+ * schema: cfg.db.schema,
49
+ * },
50
+ * auth: {
51
+ * baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:4000',
52
+ * secret: process.env.BETTER_AUTH_SECRET!,
53
+ * providers: getAuthProvidersFromEnv(),
54
+ * plugins: getAuthPluginsFromEnv(),
55
+ * },
56
+ * // Factory function - resolved fresh per request from DI container
57
+ * backend: () => container.resolve(NateraEhBackend),
58
+ * hooks: {
59
+ * onRoutesRegistered: (router) => {
60
+ * router.get('/health', (_, res) => res.send('ok'))
61
+ * },
62
+ * },
63
+ * })
64
+ * ```
65
+ */
66
+ export async function createEhMiddleware(
67
+ options: EhMiddlewareOptions,
68
+ ): Promise<EhMiddlewareResult> {
69
+ // Normalize options with defaults
70
+ const basePath = options.basePath ?? '/api'
71
+ const normalizedOptions = { ...options, basePath }
72
+
73
+ // Check if database-dependent features are enabled
74
+ const features = options.features ?? {}
75
+ const iconsEnabled = features.icons !== false
76
+ const assetsEnabled = features.assets !== false
77
+ const screenshotsEnabled = features.screenshots !== false
78
+ const legacyIconEnabled = features.legacyIconEndpoint === true
79
+ const needsDatabase = iconsEnabled || assetsEnabled || screenshotsEnabled || legacyIconEnabled
80
+
81
+ // Validate database is provided when needed
82
+ if (needsDatabase && !options.database) {
83
+ throw new Error(
84
+ 'Database configuration is required when icons, assets, screenshots, or legacyIconEndpoint features are enabled. ' +
85
+ 'Either provide database config or disable these features.',
86
+ )
87
+ }
88
+
89
+ // Initialize database manager only if database config is provided
90
+ let dbManager: EhDatabaseManager | null = null
91
+ if (options.database) {
92
+ dbManager = new EhDatabaseManager(options.database)
93
+ // Initialize the client (which also sets the global singleton)
94
+ dbManager.getClient()
95
+ }
96
+
97
+ // Create auth instance
98
+ const auth = createAuth({
99
+ appName: options.auth.appName,
100
+ baseURL: options.auth.baseURL,
101
+ secret: options.auth.secret,
102
+ providers: options.auth.providers,
103
+ plugins: options.auth.plugins,
104
+ sessionExpiresIn: options.auth.sessionExpiresIn,
105
+ sessionUpdateAge: options.auth.sessionUpdateAge,
106
+ })
107
+
108
+ // Create tRPC router
109
+ const trpcRouter = createTrpcRouter(auth)
110
+
111
+ // Normalize backend provider to async factory function
112
+ const resolveBackend = createBackendResolver(options.backend)
113
+
114
+ // Create tRPC context factory
115
+ const createContext = async () => {
116
+ const companySpecificBackend = await resolveBackend()
117
+ return createEhTrpcContext({ companySpecificBackend })
118
+ }
119
+
120
+ // Create Express router
121
+ const router = Router()
122
+ router.use(express.json())
123
+
124
+ // Build middleware context for feature registration
125
+ const middlewareContext: MiddlewareContext = {
126
+ auth,
127
+ trpcRouter,
128
+ createContext,
129
+ }
130
+
131
+ // Register tRPC middleware (if enabled)
132
+ if (normalizedOptions.features?.trpc !== false) {
133
+ router.use(
134
+ `${basePath}/trpc`,
135
+ trpcExpress.createExpressMiddleware({
136
+ router: trpcRouter,
137
+ createContext,
138
+ }),
139
+ )
140
+ }
141
+
142
+ // Register all enabled features
143
+ registerFeatures(router, normalizedOptions, middlewareContext)
144
+
145
+ // Call onRoutesRegistered hook if provided
146
+ if (options.hooks?.onRoutesRegistered) {
147
+ await options.hooks.onRoutesRegistered(router)
148
+ }
149
+
150
+ return {
151
+ router,
152
+ auth,
153
+ trpcRouter,
154
+
155
+ async connect(): Promise<void> {
156
+ if (dbManager) {
157
+ await dbManager.connect()
158
+ }
159
+ if (options.hooks?.onDatabaseConnected) {
160
+ await options.hooks.onDatabaseConnected()
161
+ }
162
+ },
163
+
164
+ async disconnect(): Promise<void> {
165
+ if (options.hooks?.onDatabaseDisconnecting) {
166
+ await options.hooks.onDatabaseDisconnecting()
167
+ }
168
+ if (dbManager) {
169
+ await dbManager.disconnect()
170
+ }
171
+ },
172
+
173
+ addRoutes(callback: (router: Router) => void): void {
174
+ callback(router)
175
+ },
176
+ }
177
+ }
@@ -0,0 +1,62 @@
1
+ import { PrismaClient } from '@prisma/client'
2
+ import type { EhDatabaseConfig } from './types'
3
+ import { setDbClient } from '../db/client'
4
+
5
+ /**
6
+ * Formats a database connection URL from structured config.
7
+ */
8
+ function formatConnectionUrl(config: EhDatabaseConfig): string {
9
+ if ('url' in config) {
10
+ return config.url
11
+ }
12
+
13
+ const { host, port, database, username, password, schema = 'public' } = config
14
+ return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`
15
+ }
16
+
17
+ /**
18
+ * Internal database manager used by the middleware.
19
+ * Handles connection URL formatting and lifecycle.
20
+ */
21
+ export class EhDatabaseManager {
22
+ private client: PrismaClient | null = null
23
+ private config: EhDatabaseConfig
24
+
25
+ constructor(config: EhDatabaseConfig) {
26
+ this.config = config
27
+ }
28
+
29
+ /**
30
+ * Get or create the Prisma client instance.
31
+ * Uses lazy initialization for flexibility.
32
+ */
33
+ getClient(): PrismaClient {
34
+ if (!this.client) {
35
+ const datasourceUrl = formatConnectionUrl(this.config)
36
+
37
+ this.client = new PrismaClient({
38
+ datasourceUrl,
39
+ log:
40
+ process.env.NODE_ENV === 'development'
41
+ ? ['warn', 'error']
42
+ : ['warn', 'error'],
43
+ })
44
+
45
+ // Bridge with existing backend-core getDbClient() usage
46
+ setDbClient(this.client)
47
+ }
48
+ return this.client
49
+ }
50
+
51
+ async connect(): Promise<void> {
52
+ const client = this.getClient()
53
+ await client.$connect()
54
+ }
55
+
56
+ async disconnect(): Promise<void> {
57
+ if (this.client) {
58
+ await this.client.$disconnect()
59
+ this.client = null
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,149 @@
1
+ import type { Router } from 'express'
2
+ import { toNodeHandler } from 'better-auth/node'
3
+ import type {
4
+ EhFeatureToggles,
5
+ EhMiddlewareOptions,
6
+ MiddlewareContext,
7
+ } from './types'
8
+ import { registerIconRestController } from '../modules/icons/iconRestController'
9
+ import { registerAssetRestController } from '../modules/assets/assetRestController'
10
+ import { registerScreenshotRestController } from '../modules/assets/screenshotRestController'
11
+ import { createAdminChatHandler } from '../modules/admin/chat/createAdminChatHandler'
12
+ import { getAssetByName } from '../modules/icons/iconService'
13
+
14
+ interface FeatureRegistration {
15
+ name: keyof EhFeatureToggles
16
+ defaultEnabled: boolean
17
+ register: (
18
+ router: Router,
19
+ options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
20
+ EhMiddlewareOptions,
21
+ context: MiddlewareContext,
22
+ ) => void
23
+ }
24
+
25
+ const FEATURES: Array<FeatureRegistration> = [
26
+ {
27
+ name: 'auth',
28
+ defaultEnabled: true,
29
+ register: (router, options, ctx) => {
30
+ const basePath = options.basePath
31
+
32
+ // Explicit session endpoint handler
33
+ router.get(`${basePath}/auth/session`, async (req, res) => {
34
+ try {
35
+ const session = await ctx.auth.api.getSession({
36
+ headers: req.headers as HeadersInit,
37
+ })
38
+ if (session) {
39
+ res.json(session)
40
+ } else {
41
+ res.status(401).json({ error: 'Not authenticated' })
42
+ }
43
+ } catch (error) {
44
+ console.error('[Auth Session Error]', error)
45
+ res.status(500).json({ error: 'Internal server error' })
46
+ }
47
+ })
48
+
49
+ // Use toNodeHandler to adapt better-auth for Express/Node.js
50
+ const authHandler = toNodeHandler(ctx.auth)
51
+ router.all(`${basePath}/auth/{*any}`, authHandler)
52
+ },
53
+ },
54
+ {
55
+ name: 'icons',
56
+ defaultEnabled: true,
57
+ register: (router, options) => {
58
+ registerIconRestController(router, {
59
+ basePath: `${options.basePath}/icons`,
60
+ })
61
+ },
62
+ },
63
+ {
64
+ name: 'assets',
65
+ defaultEnabled: true,
66
+ register: (router, options) => {
67
+ registerAssetRestController(router, {
68
+ basePath: `${options.basePath}/assets`,
69
+ })
70
+ },
71
+ },
72
+ {
73
+ name: 'screenshots',
74
+ defaultEnabled: true,
75
+ register: (router, options) => {
76
+ registerScreenshotRestController(router, {
77
+ basePath: `${options.basePath}/screenshots`,
78
+ })
79
+ },
80
+ },
81
+ {
82
+ name: 'adminChat',
83
+ defaultEnabled: false, // Only enabled if adminChat config is provided
84
+ register: (router, options) => {
85
+ if (options.adminChat) {
86
+ router.post(
87
+ `${options.basePath}/admin/chat`,
88
+ createAdminChatHandler(options.adminChat),
89
+ )
90
+ }
91
+ },
92
+ },
93
+ {
94
+ name: 'legacyIconEndpoint',
95
+ defaultEnabled: false,
96
+ register: (router) => {
97
+ // Legacy endpoint at /static/icon/:icon for backwards compatibility
98
+ router.get('/static/icon/:icon', async (req, res) => {
99
+ const { icon } = req.params
100
+
101
+ if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {
102
+ res.status(400).send('Invalid icon name')
103
+ return
104
+ }
105
+
106
+ try {
107
+ const dbIcon = await getAssetByName(icon)
108
+
109
+ if (!dbIcon) {
110
+ res.status(404).send('Icon not found')
111
+ return
112
+ }
113
+
114
+ res.setHeader('Content-Type', dbIcon.mimeType)
115
+ res.setHeader('Cache-Control', 'public, max-age=86400')
116
+ res.send(dbIcon.content)
117
+ } catch (error) {
118
+ console.error('Error fetching icon:', error)
119
+ res.status(404).send('Icon not found')
120
+ }
121
+ })
122
+ },
123
+ },
124
+ ]
125
+
126
+ /**
127
+ * Registers all enabled features on the router.
128
+ */
129
+ export function registerFeatures(
130
+ router: Router,
131
+ options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
132
+ EhMiddlewareOptions,
133
+ context: MiddlewareContext,
134
+ ): void {
135
+ const toggles = options.features || {}
136
+
137
+ for (const feature of FEATURES) {
138
+ const isEnabled = toggles[feature.name] ?? feature.defaultEnabled
139
+
140
+ // Special case: adminChat is only enabled if config is provided
141
+ if (feature.name === 'adminChat' && !options.adminChat) {
142
+ continue
143
+ }
144
+
145
+ if (isEnabled) {
146
+ feature.register(router, options, context)
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,18 @@
1
+ // Main middleware factory
2
+ export { createEhMiddleware } from './createEhMiddleware'
3
+
4
+ // Types
5
+ export type {
6
+ EhDatabaseConfig,
7
+ EhAuthConfig,
8
+ EhAdminChatConfig,
9
+ EhFeatureToggles,
10
+ EhBackendProvider,
11
+ EhLifecycleHooks,
12
+ EhMiddlewareOptions,
13
+ EhMiddlewareResult,
14
+ MiddlewareContext,
15
+ } from './types'
16
+
17
+ // Database manager (for advanced use cases)
18
+ export { EhDatabaseManager } from './database'