@igstack/app-catalog-backend-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +1934 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2539 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/prisma/migrations/20250526183023_init/migration.sql +71 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +149 -0
- package/src/__tests__/dummy.test.ts +7 -0
- package/src/db/client.ts +42 -0
- package/src/db/index.ts +21 -0
- package/src/db/syncAppCatalog.ts +312 -0
- package/src/db/tableSyncMagazine.ts +32 -0
- package/src/db/tableSyncPrismaAdapter.ts +203 -0
- package/src/index.ts +126 -0
- package/src/middleware/backendResolver.ts +42 -0
- package/src/middleware/createEhMiddleware.ts +171 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +173 -0
- package/src/middleware/index.ts +43 -0
- package/src/middleware/types.ts +202 -0
- package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
- package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
- package/src/modules/appCatalog/service.ts +130 -0
- package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +187 -0
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +213 -0
- package/src/modules/approvalMethod/approvalMethodRouter.ts +169 -0
- package/src/modules/approvalMethod/slugUtils.ts +17 -0
- package/src/modules/approvalMethod/syncApprovalMethods.ts +38 -0
- package/src/modules/assets/assetRestController.ts +271 -0
- package/src/modules/assets/assetUtils.ts +114 -0
- package/src/modules/assets/screenshotRestController.ts +195 -0
- package/src/modules/assets/screenshotRouter.ts +112 -0
- package/src/modules/assets/syncAssets.ts +277 -0
- package/src/modules/assets/upsertAsset.ts +46 -0
- package/src/modules/auth/auth.ts +51 -0
- package/src/modules/auth/authProviders.ts +40 -0
- package/src/modules/auth/authRouter.ts +75 -0
- package/src/modules/auth/authorizationUtils.ts +132 -0
- package/src/modules/auth/devMockUserUtils.ts +49 -0
- package/src/modules/auth/registerAuthRoutes.ts +33 -0
- package/src/modules/icons/iconRestController.ts +171 -0
- package/src/modules/icons/iconRouter.ts +180 -0
- package/src/modules/icons/iconService.ts +73 -0
- package/src/modules/icons/iconUtils.ts +46 -0
- package/src/prisma-json-types.d.ts +34 -0
- package/src/server/controller.ts +47 -0
- package/src/server/ehStaticControllerContract.ts +19 -0
- package/src/server/ehTrpcContext.ts +26 -0
- package/src/server/trpcSetup.ts +89 -0
- package/src/types/backend/api.ts +73 -0
- package/src/types/backend/common.ts +10 -0
- package/src/types/backend/companySpecificBackend.ts +5 -0
- package/src/types/backend/dataSources.ts +25 -0
- package/src/types/backend/deployments.ts +40 -0
- package/src/types/common/app/appTypes.ts +13 -0
- package/src/types/common/app/ui/appUiTypes.ts +12 -0
- package/src/types/common/appCatalogTypes.ts +65 -0
- package/src/types/common/approvalMethodTypes.ts +149 -0
- package/src/types/common/env/envTypes.ts +7 -0
- package/src/types/common/resourceTypes.ts +8 -0
- package/src/types/common/sharedTypes.ts +5 -0
- package/src/types/index.ts +21 -0
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
import {
|
|
14
|
+
exportAsset,
|
|
15
|
+
exportCatalog,
|
|
16
|
+
importAsset,
|
|
17
|
+
importCatalog,
|
|
18
|
+
listAssets,
|
|
19
|
+
} from '../modules/appCatalogAdmin/catalogBackupController'
|
|
20
|
+
import multer from 'multer'
|
|
21
|
+
import { createMockSessionResponse } from '../modules/auth/devMockUserUtils'
|
|
22
|
+
|
|
23
|
+
interface FeatureRegistration {
|
|
24
|
+
name: keyof EhFeatureToggles
|
|
25
|
+
defaultEnabled: boolean
|
|
26
|
+
register: (
|
|
27
|
+
router: Router,
|
|
28
|
+
options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
|
|
29
|
+
EhMiddlewareOptions,
|
|
30
|
+
context: MiddlewareContext,
|
|
31
|
+
) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Optional features that can be toggled
|
|
35
|
+
const FEATURES: Array<FeatureRegistration> = [
|
|
36
|
+
{
|
|
37
|
+
name: 'auth',
|
|
38
|
+
defaultEnabled: true,
|
|
39
|
+
register: (router, options, ctx) => {
|
|
40
|
+
const basePath = options.basePath
|
|
41
|
+
|
|
42
|
+
// Explicit session endpoint handler
|
|
43
|
+
router.get(
|
|
44
|
+
`${basePath}/auth/session`,
|
|
45
|
+
async (req, res): Promise<void> => {
|
|
46
|
+
try {
|
|
47
|
+
// Check if dev mock user is configured
|
|
48
|
+
if (ctx.authConfig.devMockUser) {
|
|
49
|
+
res.json(createMockSessionResponse(ctx.authConfig.devMockUser))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const session = await ctx.auth.api.getSession({
|
|
54
|
+
headers: req.headers as HeadersInit,
|
|
55
|
+
})
|
|
56
|
+
if (session) {
|
|
57
|
+
res.json(session)
|
|
58
|
+
} else {
|
|
59
|
+
res.status(401).json({ error: 'Not authenticated' })
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[Auth Session Error]', error)
|
|
63
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Use toNodeHandler to adapt better-auth for Express/Node.js
|
|
69
|
+
const authHandler = toNodeHandler(ctx.auth)
|
|
70
|
+
router.all(`${basePath}/auth/{*any}`, authHandler)
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'adminChat',
|
|
75
|
+
defaultEnabled: false, // Only enabled if adminChat config is provided
|
|
76
|
+
register: (router, options) => {
|
|
77
|
+
if (options.adminChat) {
|
|
78
|
+
router.post(
|
|
79
|
+
`${options.basePath}/admin/chat`,
|
|
80
|
+
createAdminChatHandler(options.adminChat),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'legacyIconEndpoint',
|
|
87
|
+
defaultEnabled: false,
|
|
88
|
+
register: (router) => {
|
|
89
|
+
// Legacy endpoint at /static/icon/:icon for backwards compatibility
|
|
90
|
+
router.get('/static/icon/:icon', async (req, res) => {
|
|
91
|
+
const { icon } = req.params
|
|
92
|
+
|
|
93
|
+
if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {
|
|
94
|
+
res.status(400).send('Invalid icon name')
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const dbIcon = await getAssetByName(icon)
|
|
100
|
+
|
|
101
|
+
if (!dbIcon) {
|
|
102
|
+
res.status(404).send('Icon not found')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
res.setHeader('Content-Type', dbIcon.mimeType)
|
|
107
|
+
res.setHeader('Cache-Control', 'public, max-age=86400')
|
|
108
|
+
res.send(dbIcon.content)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error fetching icon:', error)
|
|
111
|
+
res.status(404).send('Icon not found')
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Registers all enabled features on the router.
|
|
120
|
+
*/
|
|
121
|
+
export function registerFeatures(
|
|
122
|
+
router: Router,
|
|
123
|
+
options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
|
|
124
|
+
EhMiddlewareOptions,
|
|
125
|
+
context: MiddlewareContext,
|
|
126
|
+
): void {
|
|
127
|
+
const basePath = options.basePath
|
|
128
|
+
|
|
129
|
+
// Always-on features (required for core functionality)
|
|
130
|
+
|
|
131
|
+
// Icons
|
|
132
|
+
registerIconRestController(router, {
|
|
133
|
+
basePath: `${basePath}/icons`,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Assets
|
|
137
|
+
registerAssetRestController(router, {
|
|
138
|
+
basePath: `${basePath}/assets`,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Screenshots
|
|
142
|
+
registerScreenshotRestController(router, {
|
|
143
|
+
basePath: `${basePath}/screenshots`,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Catalog backup/restore
|
|
147
|
+
const upload = multer({ storage: multer.memoryStorage() })
|
|
148
|
+
router.get(`${basePath}/catalog/backup/export`, exportCatalog)
|
|
149
|
+
router.post(`${basePath}/catalog/backup/import`, importCatalog)
|
|
150
|
+
router.get(`${basePath}/catalog/backup/assets`, listAssets)
|
|
151
|
+
router.get(`${basePath}/catalog/backup/assets/:name`, exportAsset)
|
|
152
|
+
router.post(
|
|
153
|
+
`${basePath}/catalog/backup/assets`,
|
|
154
|
+
upload.single('file'),
|
|
155
|
+
importAsset,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Optional toggleable features
|
|
159
|
+
const toggles = options.features || {}
|
|
160
|
+
|
|
161
|
+
for (const feature of FEATURES) {
|
|
162
|
+
const isEnabled = toggles[feature.name] ?? feature.defaultEnabled
|
|
163
|
+
|
|
164
|
+
// Special case: adminChat is only enabled if config is provided
|
|
165
|
+
if (feature.name === 'adminChat' && !options.adminChat) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isEnabled) {
|
|
170
|
+
feature.register(router, options, context)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware module for app-catalog backend integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides a batteries-included middleware factory that handles all backend wiring:
|
|
5
|
+
* - Database connection management
|
|
6
|
+
* - Authentication setup
|
|
7
|
+
* - tRPC router configuration
|
|
8
|
+
* - Feature registration (icons, assets, screenshots, admin chat)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const eh = await createEhMiddleware({
|
|
13
|
+
* basePath: '/api',
|
|
14
|
+
* database: { host, port, database, username, password, schema },
|
|
15
|
+
* auth: { baseURL, secret, providers },
|
|
16
|
+
* backend: myBackendImplementation,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* app.use(eh.router)
|
|
20
|
+
* await eh.connect()
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module middleware
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Main middleware factory
|
|
27
|
+
export { createEhMiddleware } from './createEhMiddleware'
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
export type {
|
|
31
|
+
EhDatabaseConfig,
|
|
32
|
+
EhAuthConfig,
|
|
33
|
+
EhAdminChatConfig,
|
|
34
|
+
EhFeatureToggles,
|
|
35
|
+
EhBackendProvider,
|
|
36
|
+
EhLifecycleHooks,
|
|
37
|
+
EhMiddlewareOptions,
|
|
38
|
+
EhMiddlewareResult,
|
|
39
|
+
MiddlewareContext,
|
|
40
|
+
} from './types'
|
|
41
|
+
|
|
42
|
+
// Database manager (for advanced use cases)
|
|
43
|
+
export { EhDatabaseManager } from './database'
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Router } from 'express'
|
|
2
|
+
import type { LanguageModel, Tool } from 'ai'
|
|
3
|
+
import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'
|
|
4
|
+
import type { AppCatalogCompanySpecificBackend } from '../types/backend/companySpecificBackend'
|
|
5
|
+
import type { BetterAuth } from '../modules/auth/auth'
|
|
6
|
+
import type { TRPCRouter } from '../server/controller'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Database connection configuration.
|
|
10
|
+
* Supports both connection URL and structured config.
|
|
11
|
+
*/
|
|
12
|
+
export type EhDatabaseConfig =
|
|
13
|
+
| { url: string }
|
|
14
|
+
| {
|
|
15
|
+
host: string
|
|
16
|
+
port: number
|
|
17
|
+
database: string
|
|
18
|
+
username: string
|
|
19
|
+
password: string
|
|
20
|
+
schema?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mock user configuration for development/testing.
|
|
25
|
+
* When provided, bypasses authentication and injects this user into all requests.
|
|
26
|
+
*/
|
|
27
|
+
export interface EhDevMockUser {
|
|
28
|
+
/** User ID */
|
|
29
|
+
id: string
|
|
30
|
+
/** User email */
|
|
31
|
+
email: string
|
|
32
|
+
/** User display name */
|
|
33
|
+
name: string
|
|
34
|
+
/** User groups (for authorization) */
|
|
35
|
+
groups: Array<string>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Auth configuration for Better Auth integration.
|
|
40
|
+
*/
|
|
41
|
+
export interface EhAuthConfig {
|
|
42
|
+
/** Base URL for auth callbacks (e.g., 'http://localhost:4000') */
|
|
43
|
+
baseURL: string
|
|
44
|
+
/** Secret for signing sessions (min 32 chars in production) */
|
|
45
|
+
secret: string
|
|
46
|
+
/** OAuth providers configuration */
|
|
47
|
+
providers?: BetterAuthOptions['socialProviders']
|
|
48
|
+
/** Additional Better Auth plugins (e.g., Okta) */
|
|
49
|
+
plugins?: Array<BetterAuthPlugin>
|
|
50
|
+
/** Session expiration in seconds (default: 30 days) */
|
|
51
|
+
sessionExpiresIn?: number
|
|
52
|
+
/** Session refresh threshold in seconds (default: 1 day) */
|
|
53
|
+
sessionUpdateAge?: number
|
|
54
|
+
/** Application name shown in auth UI */
|
|
55
|
+
appName?: string
|
|
56
|
+
/** Development mock user - bypasses auth when provided */
|
|
57
|
+
devMockUser?: EhDevMockUser
|
|
58
|
+
/** Admin group names for authorization (default: ['env_hopper_ui_super_admins']) */
|
|
59
|
+
adminGroups?: Array<string>
|
|
60
|
+
/** Okta groups claim name (e.g., 'env_hopper_ui_groups') - used to extract groups from access token JWT */
|
|
61
|
+
oktaGroupsClaim?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Admin chat (AI) configuration.
|
|
66
|
+
* When provided, enables the admin/chat endpoint.
|
|
67
|
+
*/
|
|
68
|
+
export interface EhAdminChatConfig {
|
|
69
|
+
/** AI model instance from @ai-sdk/* packages */
|
|
70
|
+
model: LanguageModel
|
|
71
|
+
/** System prompt for the AI assistant */
|
|
72
|
+
systemPrompt?: string
|
|
73
|
+
/** Custom tools available to the AI */
|
|
74
|
+
tools?: Record<string, Tool>
|
|
75
|
+
/** Validation function called before each request */
|
|
76
|
+
validateConfig?: () => void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Feature toggles for enabling/disabling specific functionality.
|
|
81
|
+
*
|
|
82
|
+
* Note: Icons, assets, screenshots, and catalog backup are always enabled.
|
|
83
|
+
* Only these optional features can be toggled:
|
|
84
|
+
*/
|
|
85
|
+
export interface EhFeatureToggles {
|
|
86
|
+
/** Enable tRPC endpoints (default: true) */
|
|
87
|
+
trpc?: boolean
|
|
88
|
+
/** Enable auth endpoints (default: true) */
|
|
89
|
+
auth?: boolean
|
|
90
|
+
/** Enable admin chat endpoint (default: true if adminChat config provided) */
|
|
91
|
+
adminChat?: boolean
|
|
92
|
+
/** Enable legacy icon endpoint at /static/icon/:icon (default: false) */
|
|
93
|
+
legacyIconEndpoint?: boolean
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Company-specific backend can be provided as:
|
|
98
|
+
* 1. Direct object implementing the interface
|
|
99
|
+
* 2. Factory function called per-request (for DI integration)
|
|
100
|
+
* 3. Async factory function
|
|
101
|
+
*/
|
|
102
|
+
export type EhBackendProvider =
|
|
103
|
+
| AppCatalogCompanySpecificBackend
|
|
104
|
+
| (() => AppCatalogCompanySpecificBackend)
|
|
105
|
+
| (() => Promise<AppCatalogCompanySpecificBackend>)
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lifecycle hooks for database and middleware events.
|
|
109
|
+
*/
|
|
110
|
+
export interface EhLifecycleHooks {
|
|
111
|
+
/** Called after database connection is established */
|
|
112
|
+
onDatabaseConnected?: () => void | Promise<void>
|
|
113
|
+
/** Called before database disconnection (for cleanup) */
|
|
114
|
+
onDatabaseDisconnecting?: () => void | Promise<void>
|
|
115
|
+
/** Called after all routes are registered - use to add custom routes */
|
|
116
|
+
onRoutesRegistered?: (router: Router) => void | Promise<void>
|
|
117
|
+
/** Custom error handler for middleware errors */
|
|
118
|
+
onError?: (error: Error, context: { path: string }) => void
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Main configuration options for the app-catalog middleware.
|
|
123
|
+
*/
|
|
124
|
+
export interface EhMiddlewareOptions {
|
|
125
|
+
/**
|
|
126
|
+
* Base path prefix for all routes (default: '/api')
|
|
127
|
+
* - tRPC: {basePath}/trpc
|
|
128
|
+
* - Auth: {basePath}/auth (note: auth basePath is hardcoded, this affects where router mounts)
|
|
129
|
+
* - Icons: {basePath}/icons
|
|
130
|
+
* - Assets: {basePath}/assets
|
|
131
|
+
* - Screenshots: {basePath}/screenshots
|
|
132
|
+
* - Admin Chat: {basePath}/admin/chat
|
|
133
|
+
*/
|
|
134
|
+
basePath?: string
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Database connection configuration (required).
|
|
138
|
+
* Backend-core manages the database for all features.
|
|
139
|
+
*/
|
|
140
|
+
database: EhDatabaseConfig
|
|
141
|
+
|
|
142
|
+
/** Auth configuration (required) */
|
|
143
|
+
auth: EhAuthConfig
|
|
144
|
+
|
|
145
|
+
/** Company-specific backend implementation (required) */
|
|
146
|
+
backend: EhBackendProvider
|
|
147
|
+
|
|
148
|
+
/** AI admin chat configuration (optional) */
|
|
149
|
+
adminChat?: EhAdminChatConfig
|
|
150
|
+
|
|
151
|
+
/** Feature toggles (all enabled by default) */
|
|
152
|
+
features?: EhFeatureToggles
|
|
153
|
+
|
|
154
|
+
/** Lifecycle hooks */
|
|
155
|
+
hooks?: EhLifecycleHooks
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Result of middleware initialization.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const eh = await createEhMiddleware({ ... })
|
|
164
|
+
*
|
|
165
|
+
* // Mount routes
|
|
166
|
+
* app.use(eh.router)
|
|
167
|
+
*
|
|
168
|
+
* // Connect to database
|
|
169
|
+
* await eh.connect()
|
|
170
|
+
*
|
|
171
|
+
* // Cleanup on shutdown
|
|
172
|
+
* process.on('SIGTERM', async () => {
|
|
173
|
+
* await eh.disconnect()
|
|
174
|
+
* })
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export interface EhMiddlewareResult {
|
|
178
|
+
/** Express router with all app-catalog routes */
|
|
179
|
+
router: Router
|
|
180
|
+
/** Better Auth instance (for extending auth functionality) */
|
|
181
|
+
auth: BetterAuth
|
|
182
|
+
/** tRPC router (for extending with custom procedures) */
|
|
183
|
+
trpcRouter: TRPCRouter
|
|
184
|
+
/** Connect to database (call during app startup) */
|
|
185
|
+
connect: () => Promise<void>
|
|
186
|
+
/** Disconnect from database (call during app shutdown) */
|
|
187
|
+
disconnect: () => Promise<void>
|
|
188
|
+
/** Add custom routes to the middleware router */
|
|
189
|
+
addRoutes: (callback: (router: Router) => void) => void
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Internal context passed to feature registration functions.
|
|
194
|
+
*/
|
|
195
|
+
export interface MiddlewareContext {
|
|
196
|
+
auth: BetterAuth
|
|
197
|
+
trpcRouter: TRPCRouter
|
|
198
|
+
createContext: () => Promise<{
|
|
199
|
+
companySpecificBackend: AppCatalogCompanySpecificBackend
|
|
200
|
+
}>
|
|
201
|
+
authConfig: EhAuthConfig
|
|
202
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { stepCountIs, streamText, tool } from 'ai'
|
|
2
|
+
import type { LanguageModel, Tool } from 'ai'
|
|
3
|
+
|
|
4
|
+
import type { Request, Response } from 'express'
|
|
5
|
+
|
|
6
|
+
export interface AdminChatHandlerOptions {
|
|
7
|
+
/** The AI model to use (from @ai-sdk/openai, @ai-sdk/anthropic, etc.) */
|
|
8
|
+
model: LanguageModel
|
|
9
|
+
/** System prompt for the AI assistant */
|
|
10
|
+
systemPrompt?: string
|
|
11
|
+
/** Tools available to the AI assistant */
|
|
12
|
+
tools?: Record<string, Tool>
|
|
13
|
+
/**
|
|
14
|
+
* Optional function to validate configuration before processing requests.
|
|
15
|
+
* Should throw an error if configuration is invalid (e.g., missing API key).
|
|
16
|
+
* @example
|
|
17
|
+
* validateConfig: () => {
|
|
18
|
+
* if (!process.env.OPENAI_API_KEY) {
|
|
19
|
+
* throw new Error('OPENAI_API_KEY is not configured')
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
validateConfig?: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TextPart {
|
|
27
|
+
type: 'text'
|
|
28
|
+
text: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface UIMessageInput {
|
|
32
|
+
role: 'user' | 'assistant' | 'system'
|
|
33
|
+
content?: string
|
|
34
|
+
parts?: Array<TextPart | { type: string }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CoreMessage {
|
|
38
|
+
role: 'user' | 'assistant' | 'system'
|
|
39
|
+
content: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function convertToCoreMessages(
|
|
43
|
+
messages: Array<UIMessageInput>,
|
|
44
|
+
): Array<CoreMessage> {
|
|
45
|
+
return messages.map((msg) => {
|
|
46
|
+
if (msg.content) {
|
|
47
|
+
return { role: msg.role, content: msg.content }
|
|
48
|
+
}
|
|
49
|
+
// Extract text from parts array (AI SDK v3 format)
|
|
50
|
+
const textContent =
|
|
51
|
+
msg.parts
|
|
52
|
+
?.filter((part): part is TextPart => part.type === 'text')
|
|
53
|
+
.map((part) => part.text)
|
|
54
|
+
.join('') ?? ''
|
|
55
|
+
return { role: msg.role, content: textContent }
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an Express handler for the admin chat endpoint.
|
|
61
|
+
*
|
|
62
|
+
* Usage in thin wrappers:
|
|
63
|
+
*
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // With OpenAI
|
|
66
|
+
* import { openai } from '@ai-sdk/openai'
|
|
67
|
+
* app.post('/api/admin/chat', createAdminChatHandler({
|
|
68
|
+
* model: openai('gpt-4o-mini'),
|
|
69
|
+
* }))
|
|
70
|
+
*
|
|
71
|
+
* // With Claude
|
|
72
|
+
* import { anthropic } from '@ai-sdk/anthropic'
|
|
73
|
+
* app.post('/api/admin/chat', createAdminChatHandler({
|
|
74
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
75
|
+
* }))
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createAdminChatHandler(options: AdminChatHandlerOptions) {
|
|
79
|
+
const {
|
|
80
|
+
model,
|
|
81
|
+
systemPrompt = 'You are a helpful admin assistant for the App Catalog application. Help users manage apps, data sources, and MCP server configurations.',
|
|
82
|
+
tools = {},
|
|
83
|
+
validateConfig,
|
|
84
|
+
} = options
|
|
85
|
+
|
|
86
|
+
return async (req: Request, res: Response) => {
|
|
87
|
+
try {
|
|
88
|
+
// Validate configuration if validator provided
|
|
89
|
+
if (validateConfig) {
|
|
90
|
+
validateConfig()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { messages } = req.body as { messages: Array<UIMessageInput> }
|
|
94
|
+
const coreMessages = convertToCoreMessages(messages)
|
|
95
|
+
|
|
96
|
+
console.log(
|
|
97
|
+
'[Admin Chat] Received messages:',
|
|
98
|
+
JSON.stringify(coreMessages, null, 2),
|
|
99
|
+
)
|
|
100
|
+
console.log('[Admin Chat] Available tools:', Object.keys(tools))
|
|
101
|
+
|
|
102
|
+
const result = streamText({
|
|
103
|
+
model,
|
|
104
|
+
system: systemPrompt,
|
|
105
|
+
messages: coreMessages,
|
|
106
|
+
tools,
|
|
107
|
+
// Allow up to 5 steps so the model can call tools and then generate a response
|
|
108
|
+
stopWhen: stepCountIs(5),
|
|
109
|
+
onFinish: (event) => {
|
|
110
|
+
console.log('[Admin Chat] Finished:', {
|
|
111
|
+
finishReason: event.finishReason,
|
|
112
|
+
usage: event.usage,
|
|
113
|
+
hasText: !!event.text,
|
|
114
|
+
textLength: event.text.length,
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Use UI message stream response which is compatible with AI SDK React hooks
|
|
120
|
+
const response = result.toUIMessageStreamResponse()
|
|
121
|
+
|
|
122
|
+
// Copy headers from the response
|
|
123
|
+
response.headers.forEach((value, key) => {
|
|
124
|
+
res.setHeader(key, value)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Pipe the stream to the response
|
|
128
|
+
if (response.body) {
|
|
129
|
+
const reader = response.body.getReader()
|
|
130
|
+
const pump = async (): Promise<void> => {
|
|
131
|
+
const { done, value } = await reader.read()
|
|
132
|
+
if (done) {
|
|
133
|
+
res.end()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
res.write(value)
|
|
137
|
+
return pump()
|
|
138
|
+
}
|
|
139
|
+
await pump()
|
|
140
|
+
} else {
|
|
141
|
+
console.error('[Admin Chat] No response body')
|
|
142
|
+
res.status(500).json({ error: 'No response from AI model' })
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('[Admin Chat] Error:', error)
|
|
146
|
+
res.status(500).json({ error: 'Failed to process chat request' })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Re-export tool helper for convenience
|
|
152
|
+
export { tool }
|