@kuckit/app-server 2.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.
@@ -0,0 +1,202 @@
1
+ import { CoreConfig, CoreCradle, ModuleSpec } from "@kuckit/sdk";
2
+ import express, { Express, NextFunction, Request, Response } from "express";
3
+ import { AwilixContainer } from "awilix";
4
+ import { Pool } from "pg";
5
+ import { Logger, UserRepository } from "@kuckit/domain";
6
+ import { StructuredLogger } from "@kuckit/infrastructure";
7
+
8
+ //#region src/types.d.ts
9
+
10
+ /**
11
+ * Extended configuration for Kuckit server applications
12
+ * Extends CoreConfig with server-specific settings
13
+ */
14
+ interface KuckitServerConfig extends CoreConfig {
15
+ port: number;
16
+ corsOrigin: string;
17
+ }
18
+ /**
19
+ * Extended cradle for Kuckit server applications
20
+ * Includes core services plus commonly-used server dependencies
21
+ */
22
+ interface KuckitServerCradle extends CoreCradle {
23
+ config: KuckitServerConfig;
24
+ dbPool: Pool;
25
+ logger: Logger & StructuredLogger;
26
+ userRepository: UserRepository;
27
+ }
28
+ type KuckitContainer = AwilixContainer<KuckitServerCradle>;
29
+ /**
30
+ * Options for configuring the Kuckit server
31
+ */
32
+ interface KuckitServerOptions {
33
+ /**
34
+ * Load server configuration from environment variables.
35
+ * Must return a config object extending KuckitServerConfig.
36
+ */
37
+ loadConfig: () => KuckitServerConfig;
38
+ /**
39
+ * Get module specifications for the application.
40
+ * Can be async for dynamic module loading.
41
+ */
42
+ getModuleSpecs: () => ModuleSpec[] | Promise<ModuleSpec[]>;
43
+ /**
44
+ * Optional hooks for customizing server behavior
45
+ */
46
+ hooks?: KuckitServerHooks;
47
+ }
48
+ /**
49
+ * Hooks for customizing server behavior at various lifecycle points
50
+ */
51
+ interface KuckitServerHooks {
52
+ /**
53
+ * Called after the Express app is created, before any middleware is added.
54
+ * Use this to add custom middleware that should run first.
55
+ */
56
+ onAppCreated?: (app: Express) => void | Promise<void>;
57
+ /**
58
+ * Called after the DI container is built and modules are loaded.
59
+ * Use this to register additional dependencies or perform post-load setup.
60
+ */
61
+ onContainerReady?: (container: KuckitContainer) => void | Promise<void>;
62
+ /**
63
+ * Called after all routes are set up, before the server starts listening.
64
+ * Use this to add custom routes or final middleware.
65
+ */
66
+ onRoutesReady?: (app: Express, container: KuckitContainer) => void | Promise<void>;
67
+ /**
68
+ * Called after the server starts listening.
69
+ * Use this for post-startup tasks like logging or health checks.
70
+ */
71
+ onServerReady?: (port: number, container: KuckitContainer) => void | Promise<void>;
72
+ /**
73
+ * Called during graceful shutdown.
74
+ * Use this to clean up custom resources before the container is disposed.
75
+ */
76
+ onShutdown?: (container: KuckitContainer) => void | Promise<void>;
77
+ }
78
+ /**
79
+ * Result of creating a Kuckit server without starting it
80
+ */
81
+ interface KuckitServer {
82
+ app: Express;
83
+ container: KuckitContainer;
84
+ start: () => Promise<{
85
+ port: number;
86
+ close: () => Promise<void>;
87
+ }>;
88
+ }
89
+ /**
90
+ * Context for oRPC procedures
91
+ */
92
+ interface KuckitContext {
93
+ di: KuckitContainer;
94
+ session: unknown;
95
+ requestId: string;
96
+ }
97
+ /**
98
+ * Extended Express Request with DI scope
99
+ */
100
+ interface KuckitRequest extends Request {
101
+ scope: AwilixContainer<KuckitServerCradle>;
102
+ }
103
+ /**
104
+ * Middleware function type for Kuckit
105
+ */
106
+ type KuckitMiddleware = (req: KuckitRequest, res: Response, next: NextFunction) => void | Promise<void>;
107
+ //#endregion
108
+ //#region src/express/server.d.ts
109
+ /**
110
+ * Cleanup container resources
111
+ */
112
+ declare const disposeContainer: (container: KuckitContainer) => Promise<void>;
113
+ /**
114
+ * Create a Kuckit server without starting it.
115
+ * Useful for testing or when you need access to the app/container before listening.
116
+ */
117
+ declare const createKuckitServer: (options: KuckitServerOptions) => Promise<KuckitServer>;
118
+ /**
119
+ * Run a Kuckit server - the main entry point for server applications.
120
+ * Creates the server and immediately starts listening.
121
+ */
122
+ declare const runKuckitServer: (options: KuckitServerOptions) => Promise<{
123
+ port: number;
124
+ close: () => Promise<void>;
125
+ }>;
126
+ /**
127
+ * Create a headless Kuckit context (DI container only, no Express app).
128
+ * Useful for CLI tools, scripts, testing, or background workers.
129
+ */
130
+ declare const createKuckitContext: (options: KuckitServerOptions) => Promise<{
131
+ container: KuckitContainer;
132
+ dispose: () => Promise<void>;
133
+ }>;
134
+ //#endregion
135
+ //#region src/express/app.d.ts
136
+ interface CreateAppOptions {
137
+ config: KuckitServerConfig;
138
+ }
139
+ /**
140
+ * Create and configure Express app with CORS
141
+ */
142
+ declare const createKuckitApp: (options: CreateAppOptions) => Express;
143
+ //#endregion
144
+ //#region src/express/middleware.d.ts
145
+ /**
146
+ * Setup container middleware that creates per-request DI scopes
147
+ */
148
+ declare const setupContainerMiddleware: (app: Express, rootContainer: KuckitContainer) => void;
149
+ //#endregion
150
+ //#region src/express/routes.d.ts
151
+ type AnyRouter = any;
152
+ /**
153
+ * Options for setting up routes
154
+ */
155
+ interface SetupRoutesOptions {
156
+ /**
157
+ * The RPC router object. Module routers should already be wired into this.
158
+ */
159
+ rpcRouter: AnyRouter;
160
+ /**
161
+ * REST router entries from modules (for streaming endpoints)
162
+ */
163
+ restRouters?: Array<{
164
+ name: string;
165
+ router: express.Router;
166
+ basePath: string;
167
+ }>;
168
+ }
169
+ /**
170
+ * Setup Better-Auth routes
171
+ */
172
+ declare const setupAuth: (app: Express) => void;
173
+ /**
174
+ * Setup oRPC handler
175
+ */
176
+ declare const setupRPC: (app: Express, options: SetupRoutesOptions) => void;
177
+ /**
178
+ * Setup OpenAPI documentation
179
+ */
180
+ declare const setupAPIReference: (app: Express, options: SetupRoutesOptions) => void;
181
+ /**
182
+ * Setup REST routers from modules
183
+ */
184
+ declare const setupModuleRestRouters: (app: Express, options: SetupRoutesOptions) => void;
185
+ /**
186
+ * Setup health check endpoint
187
+ */
188
+ declare const setupHealth: (app: Express, container: KuckitContainer) => void;
189
+ /**
190
+ * Setup Prometheus metrics endpoint
191
+ */
192
+ declare const setupMetrics: (app: Express, container: KuckitContainer) => void;
193
+ /**
194
+ * Setup static file serving (production only)
195
+ */
196
+ declare const setupStaticFiles: (app: Express, publicDir?: string) => void;
197
+ /**
198
+ * Setup error handling middleware (must be last)
199
+ */
200
+ declare const setupErrorMiddleware: (app: Express) => void;
201
+ //#endregion
202
+ export { type CreateAppOptions, type KuckitContainer, type KuckitContext, type KuckitMiddleware, type KuckitRequest, type KuckitServer, type KuckitServerConfig, type KuckitServerCradle, type KuckitServerHooks, type KuckitServerOptions, type SetupRoutesOptions, createKuckitApp, createKuckitContext, createKuckitServer, disposeContainer, runKuckitServer, setupAPIReference, setupAuth, setupContainerMiddleware, setupErrorMiddleware, setupHealth, setupMetrics, setupModuleRestRouters, setupRPC, setupStaticFiles };
package/dist/index.js ADDED
@@ -0,0 +1,341 @@
1
+ import { createKuckitContainer, loadKuckitModules } from "@kuckit/sdk";
2
+ import { appRouter } from "@kuckit/api/routers/index";
3
+ import express from "express";
4
+ import cors from "cors";
5
+ import { asValue } from "awilix";
6
+ import path from "path";
7
+ import { RPCHandler } from "@orpc/server/node";
8
+ import { OpenAPIHandler } from "@orpc/openapi/node";
9
+ import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
10
+ import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
11
+ import { onError } from "@orpc/server";
12
+ import { fromNodeHeaders, toNodeHandler } from "better-auth/node";
13
+ import { createContext } from "@kuckit/api/context";
14
+
15
+ //#region src/express/app.ts
16
+ /**
17
+ * Create and configure Express app with CORS
18
+ */
19
+ const createKuckitApp = (options) => {
20
+ const { config } = options;
21
+ const app = express();
22
+ app.use(cors({
23
+ origin: config.corsOrigin || "",
24
+ methods: [
25
+ "GET",
26
+ "POST",
27
+ "OPTIONS"
28
+ ],
29
+ allowedHeaders: ["Content-Type", "Authorization"],
30
+ credentials: true
31
+ }));
32
+ return app;
33
+ };
34
+
35
+ //#endregion
36
+ //#region src/express/middleware.ts
37
+ /**
38
+ * Setup container middleware that creates per-request DI scopes
39
+ */
40
+ const setupContainerMiddleware = (app, rootContainer) => {
41
+ app.use((req, res, next) => {
42
+ req.scope = rootContainer.createScope();
43
+ req.scope.register({ requestId: asValue(crypto.randomUUID()) });
44
+ res.on("finish", () => {
45
+ req.scope?.dispose();
46
+ });
47
+ res.on("close", () => {
48
+ req.scope?.dispose();
49
+ });
50
+ next();
51
+ });
52
+ };
53
+
54
+ //#endregion
55
+ //#region src/express/routes.ts
56
+ /**
57
+ * Middleware to add session to request scope for REST routes.
58
+ */
59
+ const createSessionMiddleware = () => {
60
+ return async (req, _res, next) => {
61
+ if (!req.scope) return next(/* @__PURE__ */ new Error("Request scope not initialized"));
62
+ const auth = req.scope.cradle.auth;
63
+ try {
64
+ if (!auth) throw new Error("Auth not available in request scope");
65
+ const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
66
+ req.scope.register({ session: asValue(session) });
67
+ next();
68
+ } catch (error) {
69
+ console.error("[REST Auth] Failed to get session:", error);
70
+ req.scope.register({ session: asValue(null) });
71
+ next();
72
+ }
73
+ };
74
+ };
75
+ /**
76
+ * Setup Better-Auth routes
77
+ */
78
+ const setupAuth = (app) => {
79
+ app.all("/api/auth{/*path}", (req, res, _next) => {
80
+ const auth = req.scope?.cradle.auth;
81
+ if (!auth) return res.status(500).json({ error: "Auth not initialized" });
82
+ return toNodeHandler(auth)(req, res);
83
+ });
84
+ };
85
+ /**
86
+ * Setup oRPC handler
87
+ */
88
+ const setupRPC = (app, options) => {
89
+ const rpcHandler = new RPCHandler(options.rpcRouter, { interceptors: [onError((error) => {
90
+ console.error("[RPC Error]", error);
91
+ })] });
92
+ app.use(async (req, res, next) => {
93
+ if ((await rpcHandler.handle(req, res, {
94
+ prefix: "/rpc",
95
+ context: await createContext({ req })
96
+ })).matched) return;
97
+ next();
98
+ });
99
+ };
100
+ /**
101
+ * Setup OpenAPI documentation
102
+ */
103
+ const setupAPIReference = (app, options) => {
104
+ const apiHandler = new OpenAPIHandler(options.rpcRouter, {
105
+ plugins: [new OpenAPIReferencePlugin({ schemaConverters: [new ZodToJsonSchemaConverter()] })],
106
+ interceptors: [onError((error) => {
107
+ console.error("[API Reference Error]", error);
108
+ })]
109
+ });
110
+ app.use(async (req, res, next) => {
111
+ if ((await apiHandler.handle(req, res, {
112
+ prefix: "/api-reference",
113
+ context: await createContext({ req })
114
+ })).matched) return;
115
+ next();
116
+ });
117
+ };
118
+ /**
119
+ * Setup REST routers from modules
120
+ */
121
+ const setupModuleRestRouters = (app, options) => {
122
+ const routers = options.restRouters ?? [];
123
+ const sessionMiddleware = createSessionMiddleware();
124
+ for (const { name, router, basePath } of routers) {
125
+ app.use(`/api${basePath}`, express.json(), sessionMiddleware, router);
126
+ console.log(`[REST] Mounted module router: /api${basePath} (${name})`);
127
+ }
128
+ };
129
+ /**
130
+ * Setup health check endpoint
131
+ */
132
+ const setupHealth = (app, container) => {
133
+ app.get("/", (_req, res) => {
134
+ res.status(200).send("OK");
135
+ });
136
+ app.get("/health", async (_req, res) => {
137
+ const { dbPool } = container.cradle;
138
+ try {
139
+ const client = await dbPool.connect();
140
+ await client.query("SELECT 1");
141
+ client.release();
142
+ res.status(200).json({
143
+ status: "healthy",
144
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
145
+ checks: { database: "ok" }
146
+ });
147
+ } catch (error) {
148
+ console.error("[Health] Database check failed:", error);
149
+ res.status(503).json({
150
+ status: "unhealthy",
151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
152
+ checks: { database: "failed" }
153
+ });
154
+ }
155
+ });
156
+ };
157
+ /**
158
+ * Setup Prometheus metrics endpoint
159
+ */
160
+ const setupMetrics = (app, container) => {
161
+ app.get("/metrics", (_req, res) => {
162
+ const { logger } = container.cradle;
163
+ const metricsText = logger.getMetrics?.() ?? "";
164
+ res.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
165
+ res.send(metricsText);
166
+ });
167
+ };
168
+ /**
169
+ * Setup static file serving (production only)
170
+ */
171
+ const setupStaticFiles = (app, publicDir) => {
172
+ if (process.env.NODE_ENV !== "production") return;
173
+ const dir = publicDir ?? path.join(__dirname, "public");
174
+ app.use(express.static(dir, {
175
+ maxAge: "1d",
176
+ etag: true,
177
+ setHeaders: (res) => {
178
+ res.setHeader("Access-Control-Allow-Origin", "*");
179
+ }
180
+ }));
181
+ app.get("{/*path}", (req, res, next) => {
182
+ if (req.path.startsWith("/api") || req.path.startsWith("/rpc") || req.path.startsWith("/auth") || req.path.startsWith("/health") || req.path.startsWith("/metrics")) return next();
183
+ res.sendFile(path.join(dir, "index.html"));
184
+ });
185
+ };
186
+ /**
187
+ * Setup error handling middleware (must be last)
188
+ */
189
+ const setupErrorMiddleware = (app) => {
190
+ app.use((err, req, res, _next) => {
191
+ const errorHandler = req.scope?.cradle.errorHandler;
192
+ if (!errorHandler) {
193
+ res.status(500).json({
194
+ code: "INTERNAL_SERVER_ERROR",
195
+ message: "Internal server error"
196
+ });
197
+ return;
198
+ }
199
+ const serialized = errorHandler.handle(err, {
200
+ path: req.path,
201
+ method: req.method,
202
+ requestId: req.scope?.cradle.requestId
203
+ });
204
+ res.status(serialized.statusCode).json(serialized);
205
+ });
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/express/server.ts
210
+ /**
211
+ * Build the root container with all modules loaded
212
+ */
213
+ const buildContainer = async (options) => {
214
+ const config = options.loadConfig();
215
+ const moduleSpecs = await options.getModuleSpecs();
216
+ const rpcRouter = { ...appRouter };
217
+ const restRouters = [];
218
+ const container = await createKuckitContainer({ config });
219
+ await loadKuckitModules({
220
+ container,
221
+ env: config.env,
222
+ modules: moduleSpecs,
223
+ onApiRegistrations: (registrations) => {
224
+ for (const reg of registrations) if (reg.type === "rpc-router") {
225
+ if (rpcRouter[reg.name]) throw new Error(`Duplicate RPC router name: "${reg.name}"`);
226
+ rpcRouter[reg.name] = reg.router;
227
+ } else if (reg.type === "rest-router") {
228
+ const basePath = reg.prefix ?? `/${reg.name}`;
229
+ restRouters.push({
230
+ name: reg.name,
231
+ router: reg.router,
232
+ basePath
233
+ });
234
+ }
235
+ container.resolve("logger").info(`Loaded ${registrations.length} API registrations from modules`);
236
+ },
237
+ onComplete: () => {
238
+ container.resolve("logger").info("All modules loaded successfully");
239
+ }
240
+ });
241
+ return {
242
+ container,
243
+ rpcRouter,
244
+ restRouters
245
+ };
246
+ };
247
+ /**
248
+ * Cleanup container resources
249
+ */
250
+ const disposeContainer = async (container) => {
251
+ const { dbPool } = container.cradle;
252
+ if (dbPool && typeof dbPool.end === "function") await dbPool.end();
253
+ };
254
+ /**
255
+ * Create a Kuckit server without starting it.
256
+ * Useful for testing or when you need access to the app/container before listening.
257
+ */
258
+ const createKuckitServer = async (options) => {
259
+ const config = options.loadConfig();
260
+ const { container, rpcRouter, restRouters } = await buildContainer(options);
261
+ const app = createKuckitApp({ config });
262
+ await options.hooks?.onAppCreated?.(app);
263
+ setupContainerMiddleware(app, container);
264
+ await options.hooks?.onContainerReady?.(container);
265
+ const routeOptions = {
266
+ rpcRouter,
267
+ restRouters
268
+ };
269
+ setupAuth(app);
270
+ setupRPC(app, routeOptions);
271
+ setupAPIReference(app, routeOptions);
272
+ setupModuleRestRouters(app, routeOptions);
273
+ setupMetrics(app, container);
274
+ setupHealth(app, container);
275
+ setupStaticFiles(app);
276
+ await options.hooks?.onRoutesReady?.(app, container);
277
+ setupErrorMiddleware(app);
278
+ const start = async () => {
279
+ const port = config.port;
280
+ return new Promise((resolve) => {
281
+ const server = app.listen(port, () => {
282
+ const logger = container.resolve("logger");
283
+ logger.info(`Server is running on port ${port}`);
284
+ options.hooks?.onServerReady?.(port, container);
285
+ const shutdown = async () => {
286
+ logger.info("Shutting down gracefully...");
287
+ await options.hooks?.onShutdown?.(container);
288
+ server.close(async () => {
289
+ await disposeContainer(container);
290
+ logger.info("Server closed");
291
+ process.exit(0);
292
+ });
293
+ setTimeout(() => {
294
+ logger.error("Forced shutdown");
295
+ process.exit(1);
296
+ }, 1e4);
297
+ };
298
+ process.on("SIGTERM", shutdown);
299
+ process.on("SIGINT", shutdown);
300
+ resolve({
301
+ port,
302
+ close: async () => {
303
+ await options.hooks?.onShutdown?.(container);
304
+ return new Promise((res) => {
305
+ server.close(async () => {
306
+ await disposeContainer(container);
307
+ res();
308
+ });
309
+ });
310
+ }
311
+ });
312
+ });
313
+ });
314
+ };
315
+ return {
316
+ app,
317
+ container,
318
+ start
319
+ };
320
+ };
321
+ /**
322
+ * Run a Kuckit server - the main entry point for server applications.
323
+ * Creates the server and immediately starts listening.
324
+ */
325
+ const runKuckitServer = async (options) => {
326
+ return (await createKuckitServer(options)).start();
327
+ };
328
+ /**
329
+ * Create a headless Kuckit context (DI container only, no Express app).
330
+ * Useful for CLI tools, scripts, testing, or background workers.
331
+ */
332
+ const createKuckitContext = async (options) => {
333
+ const { container } = await buildContainer(options);
334
+ return {
335
+ container,
336
+ dispose: () => disposeContainer(container)
337
+ };
338
+ };
339
+
340
+ //#endregion
341
+ export { createKuckitApp, createKuckitContext, createKuckitServer, disposeContainer, runKuckitServer, setupAPIReference, setupAuth, setupContainerMiddleware, setupErrorMiddleware, setupHealth, setupMetrics, setupModuleRestRouters, setupRPC, setupStaticFiles };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@kuckit/app-server",
3
+ "version": "2.0.1",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.ts",
13
+ "default": "./src/index.ts"
14
+ },
15
+ "./*": {
16
+ "types": "./src/*.ts",
17
+ "default": "./src/*.ts"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./*": {
29
+ "types": "./dist/*.d.ts",
30
+ "default": "./dist/*.js"
31
+ }
32
+ }
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "check-types": "tsc -b"
37
+ },
38
+ "dependencies": {
39
+ "@kuckit/sdk": "workspace:*",
40
+ "@kuckit/api": "workspace:*",
41
+ "@kuckit/db": "workspace:*",
42
+ "@kuckit/domain": "workspace:*",
43
+ "@kuckit/infrastructure": "workspace:*",
44
+ "@kuckit/auth": "workspace:*",
45
+ "express": "^5.1.0",
46
+ "cors": "^2.8.5",
47
+ "awilix": "^12.0.5",
48
+ "@orpc/server": "catalog:",
49
+ "@orpc/openapi": "catalog:",
50
+ "better-auth": "catalog:",
51
+ "pg": "^8.11.3"
52
+ },
53
+ "devDependencies": {
54
+ "typescript": "catalog:",
55
+ "@types/express": "catalog:",
56
+ "@types/cors": "^2.8.17",
57
+ "@types/pg": "^8.10.9",
58
+ "tsdown": "catalog:"
59
+ },
60
+ "peerDependencies": {
61
+ "typescript": "^5"
62
+ }
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ // Main entry points
2
+ export {
3
+ runKuckitServer,
4
+ createKuckitServer,
5
+ createKuckitContext,
6
+ disposeContainer,
7
+ } from './express/server'
8
+
9
+ // Express utilities
10
+ export { createKuckitApp, type CreateAppOptions } from './express/app'
11
+ export { setupContainerMiddleware } from './express/middleware'
12
+
13
+ // Route setup functions (for custom wiring)
14
+ export {
15
+ setupAuth,
16
+ setupRPC,
17
+ setupAPIReference,
18
+ setupModuleRestRouters,
19
+ setupHealth,
20
+ setupMetrics,
21
+ setupStaticFiles,
22
+ setupErrorMiddleware,
23
+ type SetupRoutesOptions,
24
+ } from './express/routes'
25
+
26
+ // Types
27
+ export type {
28
+ KuckitServerConfig,
29
+ KuckitServerCradle,
30
+ KuckitContainer,
31
+ KuckitServerOptions,
32
+ KuckitServerHooks,
33
+ KuckitServer,
34
+ KuckitContext,
35
+ KuckitRequest,
36
+ KuckitMiddleware,
37
+ } from './types'