@donkeylabs/server 0.3.0 → 0.3.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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +6 -6
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +551 -0
  30. package/src/handlers.ts +14 -110
  31. package/src/index.ts +19 -23
  32. package/src/middleware.ts +2 -5
  33. package/src/registry.ts +4 -0
  34. package/src/server.ts +354 -337
  35. package/README.md +0 -254
  36. package/cli/commands/dev.ts +0 -134
  37. package/cli/commands/generate.ts +0 -605
  38. package/cli/commands/init.ts +0 -205
  39. package/cli/commands/interactive.ts +0 -417
  40. package/cli/commands/plugin.ts +0 -192
  41. package/cli/commands/route.ts +0 -195
  42. package/cli/donkeylabs +0 -2
  43. package/cli/index.ts +0 -114
  44. package/docs/svelte-frontend.md +0 -324
  45. package/docs/testing.md +0 -438
  46. package/mcp/donkeylabs-mcp +0 -3238
  47. package/mcp/server.ts +0 -3238
package/src/server.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { PluginManager, type CoreServices, type ConfiguredPlugin } from "./core";
3
- import { Router as RouterImpl, type IRouter, type RouteDefinition, type ServerContext } from "./router";
4
- import { Handlers, type HandlerRuntime } from "./handlers";
3
+ import { type IRouter, type RouteDefinition, type ServerContext } from "./router";
4
+ import { Handlers } from "./handlers";
5
5
  import type { MiddlewareRuntime, MiddlewareDefinition } from "./middleware";
6
6
  import {
7
7
  createLogger,
@@ -22,14 +22,13 @@ import {
22
22
  type SSEConfig,
23
23
  type RateLimiterConfig,
24
24
  type ErrorsConfig,
25
- type JobDefinition,
26
- type CronTaskDefinition,
27
25
  } from "./core/index";
28
26
 
29
27
  export interface ServerConfig {
30
28
  port?: number;
31
29
  db: CoreServices["db"];
32
30
  config?: Record<string, any>;
31
+ // Core service configurations
33
32
  logger?: LoggerConfig;
34
33
  cache?: CacheConfig;
35
34
  events?: EventsConfig;
@@ -40,54 +39,22 @@ export interface ServerConfig {
40
39
  errors?: ErrorsConfig;
41
40
  }
42
41
 
43
- export interface ServerInspection {
44
- port: number;
45
- plugins: Array<{
46
- name: string;
47
- version?: string;
48
- dependencies: string[];
49
- hasHandlers: boolean;
50
- hasMiddleware: boolean;
51
- hasEvents: boolean;
52
- hasSseChannels: boolean;
53
- hasJobs: boolean;
54
- hasCronTasks: boolean;
55
- hasCustomErrors: boolean;
56
- }>;
57
- routes: Array<{ name: string; handler: string }>;
58
- handlers: string[];
59
- events: string[];
60
- sseChannels: string[];
61
- jobs: Array<{ name: string; pluginName?: string; description?: string }>;
62
- cronTasks: Array<{
63
- id: string;
64
- name: string;
65
- expression: string;
66
- enabled: boolean;
67
- pluginName?: string;
68
- description?: string;
69
- lastRun?: Date;
70
- nextRun?: Date;
71
- }>;
72
- rateLimitRules: Array<{ pattern: string; limit: number; window: string | number }>;
73
- }
74
-
75
42
  export class AppServer {
76
43
  private port: number;
77
44
  private manager: PluginManager;
78
45
  private routers: IRouter[] = [];
79
46
  private routeMap: Map<string, RouteDefinition> = new Map();
80
- private customHandlers: Map<string, HandlerRuntime<any>> = new Map();
81
47
  private coreServices: CoreServices;
82
48
 
83
49
  constructor(options: ServerConfig) {
84
50
  this.port = options.port ?? 3000;
85
51
 
52
+ // Initialize core services
86
53
  const logger = createLogger(options.logger);
87
54
  const cache = createCache(options.cache);
88
55
  const events = createEvents(options.events);
89
56
  const cron = createCron(options.cron);
90
- const jobs = createJobs({ ...options.jobs, events });
57
+ const jobs = createJobs({ ...options.jobs, events }); // Jobs can emit events
91
58
  const sse = createSSE(options.sse);
92
59
  const rateLimiter = createRateLimiter(options.rateLimiter);
93
60
  const errors = createErrors(options.errors);
@@ -108,248 +75,58 @@ export class AppServer {
108
75
  this.manager = new PluginManager(this.coreServices);
109
76
  }
110
77
 
111
- registerPlugin(plugin: ConfiguredPlugin): this {
112
- this.manager.register(plugin);
113
- return this;
114
- }
115
-
116
78
  /**
117
- * Register a custom handler for use in routes.
118
- *
119
- * @example
120
- * server.registerHandler("echo", EchoHandler);
121
- * // Then in routes:
122
- * router.route("test").echo({ handle: ... });
79
+ * Register a plugin.
80
+ * For plugins with config, call the plugin factory first: registerPlugin(authPlugin({ key: "..." }))
81
+ * Plugins are initialized in dependency order when start() is called.
123
82
  */
124
- registerHandler(name: string, handler: HandlerRuntime<any>): this {
125
- this.customHandlers.set(name, handler);
126
- return this;
127
- }
128
-
129
- /**
130
- * Register an event schema for validation.
131
- * Events must be registered before they can be emitted.
132
- *
133
- * @example
134
- * server.registerEvent("user.created", z.object({ userId: z.string() }));
135
- */
136
- registerEvent<T>(name: string, schema: z.ZodType<T>): this {
137
- this.coreServices.events.register(name, schema);
138
- return this;
139
- }
140
-
141
- /**
142
- * Register multiple event schemas.
143
- */
144
- registerEvents(schemas: Record<string, z.ZodType<any>>): this {
145
- this.coreServices.events.registerMany(schemas);
146
- return this;
147
- }
148
-
149
- /**
150
- * Register an SSE channel with optional event schemas.
151
- * Channels must be registered before broadcasting.
152
- *
153
- * @example
154
- * server.registerSSEChannel("notifications", {
155
- * events: { "new": z.object({ id: z.string(), message: z.string() }) }
156
- * });
157
- */
158
- registerSSEChannel(name: string, config?: { events?: Record<string, z.ZodType<any>>; pattern?: boolean }): this {
159
- this.coreServices.sse.registerChannel(name, config);
160
- return this;
161
- }
162
-
163
- /**
164
- * Register multiple SSE channels.
165
- */
166
- registerSSEChannels(channels: Record<string, { events?: Record<string, z.ZodType<any>>; pattern?: boolean }>): this {
167
- this.coreServices.sse.registerChannels(channels);
168
- return this;
169
- }
170
-
171
- /**
172
- * Register a rate limit rule for a route pattern.
173
- * Rules are auto-enforced in the request handler.
174
- *
175
- * @example
176
- * server.registerRateLimit("auth.login", { limit: 5, window: "15m" });
177
- * server.registerRateLimit("api.*", { limit: 100, window: "1m" });
178
- */
179
- registerRateLimit(pattern: string, rule: {
180
- limit: number;
181
- window: string | number;
182
- keyFn?: (ctx: { ip: string; route: string; user?: any }) => string;
183
- skip?: (ctx: { ip: string; route: string; user?: any }) => boolean;
184
- message?: string;
185
- }): this {
186
- this.coreServices.rateLimiter.registerRule(pattern, rule);
187
- return this;
188
- }
189
-
190
- /**
191
- * Register multiple rate limit rules.
192
- */
193
- registerRateLimits(rules: Record<string, {
194
- limit: number;
195
- window: string | number;
196
- keyFn?: (ctx: { ip: string; route: string; user?: any }) => string;
197
- skip?: (ctx: { ip: string; route: string; user?: any }) => boolean;
198
- message?: string;
199
- }>): this {
200
- this.coreServices.rateLimiter.registerRules(rules);
201
- return this;
202
- }
203
-
204
- /**
205
- * Register a job with schema validation.
206
- *
207
- * @example
208
- * server.registerJob("email.send", {
209
- * schema: z.object({ to: z.string().email(), subject: z.string() }),
210
- * handler: async (data) => { await sendEmail(data); }
211
- * });
212
- */
213
- registerJob<T = any, R = any>(name: string, definition: JobDefinition<T, R>): this {
214
- this.coreServices.jobs.registerJob(name, definition);
215
- return this;
216
- }
217
-
218
- /**
219
- * Register multiple jobs at once.
220
- */
221
- registerJobs(jobs: Record<string, JobDefinition>): this {
222
- this.coreServices.jobs.registerJobs(jobs);
83
+ registerPlugin(plugin: ConfiguredPlugin): this {
84
+ this.manager.register(plugin);
223
85
  return this;
224
86
  }
225
87
 
226
88
  /**
227
- * Register a cron task.
228
- *
229
- * @example
230
- * server.registerCronTask("cleanup.daily", {
231
- * expression: "0 0 * * *",
232
- * handler: async () => { await cleanupOldRecords(); },
233
- * description: "Clean up old records daily at midnight"
234
- * });
89
+ * Add a router to handle RPC routes.
235
90
  */
236
- registerCronTask(name: string, definition: CronTaskDefinition): this {
237
- this.coreServices.cron.registerTask(name, definition);
91
+ use(router: IRouter): this {
92
+ this.routers.push(router);
238
93
  return this;
239
94
  }
240
95
 
241
96
  /**
242
- * Register multiple cron tasks at once.
97
+ * Get plugin services (for testing or advanced use cases).
243
98
  */
244
- registerCronTasks(tasks: Record<string, CronTaskDefinition>): this {
245
- this.coreServices.cron.registerTasks(tasks);
246
- return this;
247
- }
248
-
249
- /** Create a new router with prefix or register an existing router */
250
- router(prefixOrRouter: string | IRouter): IRouter {
251
- if (typeof prefixOrRouter === "string") {
252
- const newRouter = new RouterImpl(prefixOrRouter);
253
- this.routers.push(newRouter);
254
- return newRouter;
255
- }
256
- this.routers.push(prefixOrRouter);
257
- return prefixOrRouter;
258
- }
259
-
260
99
  getServices(): any {
261
100
  return this.manager.getServices();
262
101
  }
263
102
 
264
- getDb(): CoreServices["db"] {
265
- return this.manager.getCore().db;
266
- }
267
-
268
103
  /**
269
- * Introspect all registered services, handlers, routes, and plugins.
270
- * Useful for debugging and documentation generation.
104
+ * Get the database instance.
271
105
  */
272
- inspect(): ServerInspection {
273
- const plugins = this.manager.getPlugins().map(p => ({
274
- name: p.name,
275
- version: p.version,
276
- dependencies: p.dependencies ? [...p.dependencies] : [],
277
- hasHandlers: !!p.handlers && Object.keys(p.handlers).length > 0,
278
- hasMiddleware: !!p.middleware,
279
- hasEvents: !!p.events && Object.keys(p.events).length > 0,
280
- hasSseChannels: !!p.sseChannels && Object.keys(p.sseChannels).length > 0,
281
- hasJobs: !!p.jobs && Object.keys(p.jobs).length > 0,
282
- hasCronTasks: !!p.cronTasks && Object.keys(p.cronTasks).length > 0,
283
- hasCustomErrors: !!p.customErrors && Object.keys(p.customErrors).length > 0,
284
- }));
285
-
286
- const routes = this.getRoutes();
287
-
288
- const handlers = [
289
- ...Object.keys(Handlers),
290
- ...Array.from(this.customHandlers.keys()),
291
- ...this.manager.getPlugins().flatMap(p =>
292
- p.handlers ? Object.keys(p.handlers) : []
293
- ),
294
- ];
295
-
296
- const events = this.coreServices.events.listRegistered();
297
-
298
- const sseChannels = this.coreServices.sse.listChannels
299
- ? this.coreServices.sse.listChannels()
300
- : [];
301
-
302
- const jobs = this.coreServices.jobs.listRegisteredJobs();
303
-
304
- const cronTasks = this.coreServices.cron.list();
305
-
306
- const rateLimitRules = this.coreServices.rateLimiter.listRules
307
- ? this.coreServices.rateLimiter.listRules()
308
- : [];
309
-
310
- return {
311
- port: this.port,
312
- plugins,
313
- routes,
314
- handlers: [...new Set(handlers)],
315
- events,
316
- sseChannels,
317
- jobs: jobs.map(j => ({
318
- name: j.name,
319
- pluginName: j.pluginName,
320
- description: j.description,
321
- })),
322
- cronTasks: cronTasks.map(t => ({
323
- id: t.id,
324
- name: t.name,
325
- expression: t.expression,
326
- enabled: t.enabled,
327
- pluginName: t.pluginName,
328
- description: t.description,
329
- lastRun: t.lastRun,
330
- nextRun: t.nextRun,
331
- })),
332
- rateLimitRules,
333
- };
106
+ getDb(): CoreServices["db"] {
107
+ return this.manager.getCore().db;
334
108
  }
335
109
 
110
+ // Resolve middleware runtime from plugins
336
111
  private resolveMiddleware(name: string): MiddlewareRuntime<any> | undefined {
337
112
  for (const plugin of this.manager.getPlugins()) {
338
- // Use resolved middleware (from calling middleware function during init)
339
- const resolvedMiddleware = (plugin as any)._resolvedMiddleware;
340
- if (resolvedMiddleware && resolvedMiddleware[name]) {
341
- return resolvedMiddleware[name] as MiddlewareRuntime<any>;
113
+ // Middleware is resolved and stored in _resolvedMiddleware during plugin init
114
+ const resolved = (plugin as any)._resolvedMiddleware as Record<string, MiddlewareRuntime<any>> | undefined;
115
+ if (resolved && resolved[name]) {
116
+ return resolved[name];
342
117
  }
343
118
  }
344
119
  return undefined;
345
120
  }
346
121
 
122
+ // Execute middleware chain, then call final handler
347
123
  private async executeMiddlewareChain(
348
124
  req: Request,
349
125
  ctx: ServerContext,
350
126
  stack: MiddlewareDefinition[],
351
127
  finalHandler: () => Promise<Response>
352
128
  ): Promise<Response> {
129
+ // Build chain from end to start (last middleware wraps first)
353
130
  let next = finalHandler;
354
131
 
355
132
  for (let i = stack.length - 1; i >= 0; i--) {
@@ -371,41 +148,257 @@ export class AppServer {
371
148
  return next();
372
149
  }
373
150
 
151
+ /**
152
+ * Get core services (for advanced use cases).
153
+ */
374
154
  getCore(): CoreServices {
375
155
  return this.coreServices;
376
156
  }
377
157
 
378
- /** Get all registered routes for code generation */
379
- getRoutes(): { name: string; handler: string }[] {
380
- const routes: { name: string; handler: string }[] = [];
158
+ /**
159
+ * Get the internal route map for adapter introspection.
160
+ */
161
+ getRouteMap(): Map<string, RouteDefinition> {
162
+ return this.routeMap;
163
+ }
164
+
165
+ /**
166
+ * Check if a route name is registered.
167
+ */
168
+ hasRoute(routeName: string): boolean {
169
+ return this.routeMap.has(routeName);
170
+ }
171
+
172
+ /**
173
+ * Initialize server without starting HTTP server.
174
+ * Used by adapters (e.g., SvelteKit) that manage their own HTTP server.
175
+ */
176
+ async initialize(): Promise<void> {
177
+ const { logger } = this.coreServices;
178
+
179
+ await this.manager.migrate();
180
+ await this.manager.init();
181
+
182
+ this.coreServices.cron.start();
183
+ this.coreServices.jobs.start();
184
+ logger.info("Background services started (cron, jobs)");
185
+
381
186
  for (const router of this.routers) {
382
187
  for (const route of router.getRoutes()) {
383
- routes.push({
384
- name: route.name,
385
- handler: route.handler || "typed",
188
+ if (this.routeMap.has(route.name)) {
189
+ logger.warn(`Duplicate route detected`, { route: route.name });
190
+ }
191
+ this.routeMap.set(route.name, route);
192
+ }
193
+ }
194
+ logger.info(`Loaded ${this.routeMap.size} RPC routes`);
195
+ logger.info("Server initialized (adapter mode)");
196
+ }
197
+
198
+ /**
199
+ * Handle a single API request. Used by adapters.
200
+ * Returns null if the route is not found.
201
+ */
202
+ async handleRequest(
203
+ req: Request,
204
+ routeName: string,
205
+ ip: string,
206
+ options?: { corsHeaders?: Record<string, string> }
207
+ ): Promise<Response | null> {
208
+ const { logger } = this.coreServices;
209
+ const corsHeaders = options?.corsHeaders ?? {};
210
+
211
+ const route = this.routeMap.get(routeName);
212
+ if (!route) {
213
+ return null;
214
+ }
215
+
216
+ const type = route.handler || "typed";
217
+
218
+ // First check core handlers
219
+ let handler = Handlers[type as keyof typeof Handlers];
220
+
221
+ // If not found, check plugin handlers
222
+ if (!handler) {
223
+ for (const config of this.manager.getPlugins()) {
224
+ if (config.handlers && config.handlers[type]) {
225
+ handler = config.handlers[type] as any;
226
+ break;
227
+ }
228
+ }
229
+ }
230
+
231
+ if (!handler) {
232
+ logger.error("Handler not found", { handler: type, route: routeName });
233
+ return Response.json(
234
+ { error: "HANDLER_NOT_FOUND", message: "Handler not found" },
235
+ { status: 500, headers: corsHeaders }
236
+ );
237
+ }
238
+
239
+ // Build context
240
+ const ctx: ServerContext = {
241
+ db: this.coreServices.db,
242
+ plugins: this.manager.getServices(),
243
+ core: this.coreServices,
244
+ errors: this.coreServices.errors,
245
+ config: this.coreServices.config,
246
+ ip,
247
+ requestId: crypto.randomUUID(),
248
+ };
249
+
250
+ // Get middleware stack
251
+ const middlewareStack = route.middleware || [];
252
+
253
+ // Final handler
254
+ const finalHandler = async () => {
255
+ const response = await handler.execute(req, route, route.handle, ctx);
256
+ // Add CORS headers if provided
257
+ if (Object.keys(corsHeaders).length > 0 && response instanceof Response) {
258
+ const newHeaders = new Headers(response.headers);
259
+ Object.entries(corsHeaders).forEach(([k, v]) => newHeaders.set(k, v));
260
+ return new Response(response.body, {
261
+ status: response.status,
262
+ statusText: response.statusText,
263
+ headers: newHeaders,
264
+ });
265
+ }
266
+ return response;
267
+ };
268
+
269
+ try {
270
+ if (middlewareStack.length > 0) {
271
+ return await this.executeMiddlewareChain(req, ctx, middlewareStack, finalHandler);
272
+ } else {
273
+ return await finalHandler();
274
+ }
275
+ } catch (error) {
276
+ if (error instanceof HttpError) {
277
+ logger.warn("HTTP error thrown", {
278
+ route: routeName,
279
+ status: error.status,
280
+ code: error.code,
281
+ message: error.message,
282
+ });
283
+ return Response.json(error.toJSON(), {
284
+ status: error.status,
285
+ headers: corsHeaders,
386
286
  });
387
287
  }
288
+ throw error;
388
289
  }
389
- return routes;
390
290
  }
391
291
 
392
- async start(): Promise<void> {
292
+ /**
293
+ * Call a route directly without HTTP (for SSR).
294
+ * This bypasses the HTTP layer and calls the route handler directly.
295
+ *
296
+ * @param routeName - The route name (e.g., "api.counter.get")
297
+ * @param input - The input data for the route
298
+ * @param ip - Client IP address (optional, defaults to "127.0.0.1")
299
+ * @returns The route handler result
300
+ */
301
+ async callRoute<TOutput = any>(
302
+ routeName: string,
303
+ input: any,
304
+ ip: string = "127.0.0.1"
305
+ ): Promise<TOutput> {
393
306
  const { logger } = this.coreServices;
394
307
 
395
- // If running in generate mode, export routes and exit
396
- if (process.env.DONKEYLABS_GENERATE === "1") {
397
- const routes = this.getRoutes();
398
- console.log(JSON.stringify({ routes }));
399
- process.exit(0);
308
+ const route = this.routeMap.get(routeName);
309
+ if (!route) {
310
+ throw new Error(`Route "${routeName}" not found`);
311
+ }
312
+
313
+ // Build context
314
+ const ctx: ServerContext = {
315
+ db: this.coreServices.db,
316
+ plugins: this.manager.getServices(),
317
+ core: this.coreServices,
318
+ errors: this.coreServices.errors,
319
+ config: this.coreServices.config,
320
+ ip,
321
+ requestId: crypto.randomUUID(),
322
+ };
323
+
324
+ // Validate input if schema exists
325
+ if (route.input) {
326
+ const result = route.input.safeParse(input);
327
+ if (!result.success) {
328
+ throw new HttpError(400, "VALIDATION_ERROR", result.error.message);
329
+ }
330
+ input = result.data;
331
+ }
332
+
333
+ // Execute through middleware chain if present
334
+ const middlewareStack = route.middleware || [];
335
+
336
+ const finalHandler = async () => {
337
+ return route.handle(input, ctx);
338
+ };
339
+
340
+ try {
341
+ if (middlewareStack.length > 0) {
342
+ // Create a fake request for middleware compatibility
343
+ const fakeReq = new Request("http://localhost/" + routeName, {
344
+ method: "POST",
345
+ body: JSON.stringify(input),
346
+ });
347
+ const response = await this.executeMiddlewareChain(
348
+ fakeReq,
349
+ ctx,
350
+ middlewareStack,
351
+ async () => {
352
+ const result = await finalHandler();
353
+ // Return as Response for middleware chain, we'll extract later
354
+ return Response.json(result);
355
+ }
356
+ );
357
+ // Extract result from Response
358
+ if (response instanceof Response) {
359
+ return response.json();
360
+ }
361
+ return response;
362
+ } else {
363
+ return await finalHandler();
364
+ }
365
+ } catch (error) {
366
+ if (error instanceof HttpError) {
367
+ logger.warn("Route error (SSR)", {
368
+ route: routeName,
369
+ status: error.status,
370
+ code: error.code,
371
+ });
372
+ // Re-throw as a proper error for SSR
373
+ throw error;
374
+ }
375
+ throw error;
400
376
  }
377
+ }
378
+
379
+ /**
380
+ * Start the server.
381
+ * This will:
382
+ * 1. Run all plugin migrations
383
+ * 2. Initialize all plugins in dependency order
384
+ * 3. Start cron and jobs services
385
+ * 4. Start the HTTP server
386
+ */
387
+ async start() {
388
+ const { logger } = this.coreServices;
401
389
 
390
+ // 1. Run migrations
402
391
  await this.manager.migrate();
392
+
393
+ // 2. Initialize plugins
403
394
  await this.manager.init();
404
395
 
396
+ // 3. Start background services
405
397
  this.coreServices.cron.start();
406
398
  this.coreServices.jobs.start();
407
399
  logger.info("Background services started (cron, jobs)");
408
400
 
401
+ // 4. Build route map
409
402
  for (const router of this.routers) {
410
403
  for (const route of router.getRoutes()) {
411
404
  if (this.routeMap.has(route.name)) {
@@ -416,109 +409,133 @@ export class AppServer {
416
409
  }
417
410
  logger.info(`Loaded ${this.routeMap.size} RPC routes`);
418
411
 
412
+ // 5. Start HTTP server
419
413
  Bun.serve({
420
414
  port: this.port,
421
415
  fetch: async (req, server) => {
422
416
  const url = new URL(req.url);
417
+
418
+ // Extract client IP
423
419
  const ip = extractClientIP(req, server.requestIP(req)?.address);
424
420
 
425
- if (req.method !== "POST") {
426
- return new Response("Method Not Allowed", { status: 405 });
421
+ // Handle SSE endpoint
422
+ if (url.pathname === "/sse" && req.method === "GET") {
423
+ return this.handleSSE(req, ip);
427
424
  }
428
425
 
429
- const actionName = url.pathname.slice(1);
430
- const route = this.routeMap.get(actionName);
426
+ // We only allow POST for RPC routes
427
+ if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
431
428
 
432
- if (!route) {
433
- return new Response("Not Found", { status: 404 });
434
- }
429
+ // Extract action from URL path (e.g., "auth.login")
430
+ const actionName = url.pathname.slice(1);
435
431
 
436
- // Auto rate limit check against registered rules
437
- const rateLimitResult = await this.coreServices.rateLimiter.checkRoute(actionName, { ip });
438
- if (rateLimitResult && !rateLimitResult.allowed) {
439
- const rule = this.coreServices.rateLimiter.matchRoute(actionName);
440
- return Response.json(
441
- {
442
- error: "TOO_MANY_REQUESTS",
443
- message: rule?.message ?? "Rate limit exceeded. Please try again later.",
444
- retryAfter: rateLimitResult.retryAfter,
445
- },
446
- {
447
- status: 429,
448
- headers: {
449
- "Retry-After": String(rateLimitResult.retryAfter),
450
- "X-RateLimit-Limit": String(rateLimitResult.limit),
451
- "X-RateLimit-Remaining": String(rateLimitResult.remaining),
452
- "X-RateLimit-Reset": rateLimitResult.resetAt.toISOString(),
453
- },
432
+ const route = this.routeMap.get(actionName);
433
+ if (route) {
434
+ const type = route.handler || "typed";
435
+
436
+ // First check core handlers
437
+ let handler = Handlers[type as keyof typeof Handlers];
438
+
439
+ // If not found, check plugin handlers
440
+ if (!handler) {
441
+ for (const config of this.manager.getPlugins()) {
442
+ if (config.handlers && config.handlers[type]) {
443
+ handler = config.handlers[type] as any;
444
+ break;
445
+ }
454
446
  }
455
- );
456
- }
457
-
458
- const type = route.handler || "typed";
459
- let handler: HandlerRuntime<any> | undefined = Handlers[type as keyof typeof Handlers];
460
-
461
- // Check user-registered handlers
462
- if (!handler) {
463
- handler = this.customHandlers.get(type);
464
- }
447
+ }
465
448
 
466
- // Check plugin handlers
467
- if (!handler) {
468
- for (const plugin of this.manager.getPlugins()) {
469
- if (plugin.handlers && plugin.handlers[type]) {
470
- handler = plugin.handlers[type] as any;
471
- break;
449
+ if (handler) {
450
+ // Build context with core services and IP
451
+ const ctx: ServerContext = {
452
+ db: this.coreServices.db,
453
+ plugins: this.manager.getServices(),
454
+ core: this.coreServices,
455
+ errors: this.coreServices.errors, // Convenience access
456
+ config: this.coreServices.config,
457
+ ip,
458
+ requestId: crypto.randomUUID(),
459
+ };
460
+
461
+ // Get middleware stack for this route
462
+ const middlewareStack = route.middleware || [];
463
+
464
+ // Final handler execution
465
+ const finalHandler = async () => {
466
+ return await handler.execute(req, route, route.handle, ctx);
467
+ };
468
+
469
+ // Execute middleware chain, then handler - with HttpError handling
470
+ try {
471
+ if (middlewareStack.length > 0) {
472
+ return await this.executeMiddlewareChain(req, ctx, middlewareStack, finalHandler);
473
+ } else {
474
+ return await finalHandler();
475
+ }
476
+ } catch (error) {
477
+ // Handle HttpError (thrown via ctx.errors.*)
478
+ if (error instanceof HttpError) {
479
+ logger.warn("HTTP error thrown", {
480
+ route: actionName,
481
+ status: error.status,
482
+ code: error.code,
483
+ message: error.message,
484
+ });
485
+ return Response.json(error.toJSON(), { status: error.status });
486
+ }
487
+ // Re-throw unknown errors
488
+ throw error;
472
489
  }
490
+ } else {
491
+ logger.error("Handler not found", { handler: type, route: actionName });
492
+ return new Response("Handler Not Found", { status: 500 });
473
493
  }
474
494
  }
475
495
 
476
- if (!handler) {
477
- logger.error("Handler not found", { handler: type, route: actionName });
478
- return new Response("Handler Not Found", { status: 500 });
479
- }
480
-
481
- const ctx: ServerContext = {
482
- db: this.coreServices.db,
483
- plugins: this.manager.getServices(),
484
- core: this.coreServices,
485
- errors: this.coreServices.errors,
486
- config: this.coreServices.config,
487
- ip,
488
- requestId: crypto.randomUUID(),
489
- };
490
-
491
- const routeMiddleware = route.middleware || [];
492
- const routeHandler = async () => handler.execute(req, route, route.handle, ctx);
493
-
494
- try {
495
- if (routeMiddleware.length > 0) {
496
- return await this.executeMiddlewareChain(req, ctx, routeMiddleware, routeHandler);
497
- }
498
- return await routeHandler();
499
- } catch (error) {
500
- if (error instanceof HttpError) {
501
- logger.warn("HTTP error thrown", {
502
- route: actionName,
503
- status: error.status,
504
- code: error.code,
505
- message: error.message,
506
- });
507
- return Response.json(error.toJSON(), { status: error.status });
508
- }
509
- throw error;
510
- }
496
+ return new Response("Not Found", { status: 404 });
511
497
  }
512
498
  });
513
499
 
514
500
  logger.info(`Server running at http://localhost:${this.port}`);
515
501
  }
516
502
 
517
- async shutdown(): Promise<void> {
503
+ /**
504
+ * Handle SSE (Server-Sent Events) connections.
505
+ * Used by both standalone server and adapters.
506
+ */
507
+ handleSSE(req: Request, ip: string): Response {
508
+ const url = new URL(req.url);
509
+ const channels = url.searchParams.get("channels")?.split(",").filter(Boolean) || [];
510
+ const lastEventId = req.headers.get("last-event-id") || undefined;
511
+
512
+ const { client, response } = this.coreServices.sse.addClient({ lastEventId });
513
+
514
+ // Subscribe to requested channels
515
+ for (const channel of channels) {
516
+ this.coreServices.sse.subscribe(client.id, channel);
517
+ }
518
+
519
+ // Clean up when connection closes
520
+ req.signal.addEventListener("abort", () => {
521
+ this.coreServices.sse.removeClient(client.id);
522
+ });
523
+
524
+ return response;
525
+ }
526
+
527
+ /**
528
+ * Gracefully shutdown the server.
529
+ * Stops background services and closes SSE connections.
530
+ */
531
+ async shutdown() {
518
532
  const { logger } = this.coreServices;
519
533
  logger.info("Shutting down server...");
520
534
 
535
+ // Stop SSE (closes all client connections)
521
536
  this.coreServices.sse.shutdown();
537
+
538
+ // Stop background services
522
539
  await this.coreServices.jobs.stop();
523
540
  await this.coreServices.cron.stop();
524
541